1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.wifi.hotspot2.anqp;
18 
19 import static org.junit.Assert.assertEquals;
20 
21 import android.net.wifi.WifiSsid;
22 
23 import androidx.test.filters.SmallTest;
24 
25 import com.android.server.wifi.WifiBaseTest;
26 
27 import org.junit.Test;
28 
29 import java.io.ByteArrayOutputStream;
30 import java.io.IOException;
31 import java.net.ProtocolException;
32 import java.nio.ByteBuffer;
33 import java.nio.ByteOrder;
34 import java.nio.charset.StandardCharsets;
35 import java.util.ArrayList;
36 import java.util.Arrays;
37 import java.util.List;
38 import java.util.Locale;
39 
40 /**
41  * Unit tests for {@link com.android.server.wifi.hotspot2.anqp.ANQPParser}.
42  */
43 @SmallTest
44 public class ANQPParserTest extends WifiBaseTest {
45     /**
46      * Helper function for generating payload for a Venue Name ANQP element.
47      *
48      * @param language Array of languages
49      * @param text Array of text
50      * @return byte[]
51      * @throws IOException
52      */
getVenueNamePayload(String[] language, String[] text)53     private static byte[] getVenueNamePayload(String[] language, String[] text)
54             throws IOException {
55         ByteArrayOutputStream stream = new ByteArrayOutputStream();
56         stream.write(new byte[VenueNameElement.VENUE_INFO_LENGTH]);
57         stream.write(getI18NameListPayload(language, text));
58         return stream.toByteArray();
59     }
60 
61     /**
62      * Helper function for generating payload for a Domain Name ANQP element.
63      *
64      * @param names Array of domain names
65      * @return byte[]
66      * @throws IOException
67      */
getDomainNamePayload(String[] names)68     private static byte[] getDomainNamePayload(String[] names) throws IOException {
69         ByteArrayOutputStream stream = new ByteArrayOutputStream();
70         for (String name : names) {
71             byte[] nameBytes = name.getBytes(StandardCharsets.ISO_8859_1);
72             stream.write((byte) nameBytes.length);
73             stream.write(nameBytes);
74         }
75         return stream.toByteArray();
76     }
77 
78     /**
79      * Helper function for generating payload for a Roaming Consortium ANQP element.
80      *
81      * @param ois Array of OIs
82      * @param oisLength Array of length of each corresponding OI
83      * @return byte[]
84      * @throws IOException
85      */
getRoamingConsortiumPayload(Long[] ois, int[] oisLength)86     private static byte[] getRoamingConsortiumPayload(Long[] ois, int[] oisLength)
87             throws IOException {
88         ByteArrayOutputStream stream = new ByteArrayOutputStream();
89         for (int i = 0; i < ois.length; i++) {
90             stream.write((byte) oisLength[i]);
91             // Write the OI data in big-endian.
92             for (int l = oisLength[i] - 1; l >= 0; l--) {
93                 stream.write((byte) ((ois[i].longValue() >> l * Byte.SIZE) & 0xFF));
94             }
95         }
96         return stream.toByteArray();
97     }
98 
99     /**
100      * Helper function for generating payload for a NAI Realm ANQP element.
101      *
102      * @param realmDataList Array of realm data.
103      * @return byte[]
104      * @throws IOException
105      */
getNAIRealmPayload(byte[][] realmDataList)106     private static byte[] getNAIRealmPayload(byte[][] realmDataList) throws IOException {
107         ByteArrayOutputStream stream = new ByteArrayOutputStream();
108         // Data count in little-endian
109         stream.write((byte) (realmDataList.length & 0xFF));
110         stream.write((byte) ((realmDataList.length >> 8) & 0xFF));
111         for (byte[] realmData : realmDataList) {
112             stream.write(realmData);
113         }
114         return stream.toByteArray();
115     }
116 
117     /**
118      * Helper function for generating payload for 3GPP Network ANQP element.
119      *
120      * @param ieiList Array of IEI data
121      * @return byte[]
122      * @throws IOException
123      */
getThreeGPPNetworkPayload(byte[][] ieiList)124     private static byte[] getThreeGPPNetworkPayload(byte[][] ieiList) throws IOException {
125         ByteArrayOutputStream stream = new ByteArrayOutputStream();
126         int totalIeiSize = CellularNetworkTestUtil.getDataSize(ieiList);
127         stream.write((byte) ThreeGPPNetworkElement.GUD_VERSION_1);
128         stream.write((byte) totalIeiSize);
129         for (byte[] iei : ieiList) {
130             stream.write(iei);
131         }
132         return stream.toByteArray();
133     }
134 
135     /**
136      * Helper function for generating payload for Vendor Specific ANQP element.
137      *
138      * @param oi The OI of the vendor
139      * @param type The type of the element
140      * @param subtype The subtype of the element
141      * @param payload The vendor specific data
142      * @return byte[]
143      * @throws IOException
144      */
getVendorSpecificPayload(int oi, int type, int subtype, byte[] payload)145     private static byte[] getVendorSpecificPayload(int oi, int type, int subtype, byte[] payload)
146             throws IOException {
147         ByteArrayOutputStream stream = new ByteArrayOutputStream();
148         stream.write((byte) ((oi >> 16) & 0xFF));
149         stream.write((byte) ((oi >> 8) & 0xFF));
150         stream.write((byte) (oi & 0xFF));
151         stream.write((byte) type);
152         stream.write((byte) subtype);
153         stream.write((byte) 0);    // Reserved
154         stream.write(payload);
155         return stream.toByteArray();
156     }
157 
158     /**
159      * Helper function for generating payload for a Hotspot 2.0 Operator Friendly Name ANQP element.
160      *
161      * @param language Array of language
162      * @param text Array of text
163      * @return byte[]
164      * @throws IOException
165      */
getHSFriendlyNamePayload(String[] language, String[] text)166     private static byte[] getHSFriendlyNamePayload(String[] language, String[] text)
167             throws IOException {
168         return getI18NameListPayload(language, text);
169     }
170 
171     /**
172      * Helper function for generating payload for a Hotspot 2.0 WAN Metrics ANQP element.
173      *
174      * @param status Link status
175      * @param symmetric Flag indicating symmetric link speed
176      * @param capped Flag indicating link operating at max capacity
177      * @param downlinkSpeed Downlink speed
178      * @param uplinkSpeed Uplink speed
179      * @param downlinkLoad Downlink load
180      * @param uplinkLoad Uplink load
181      * @param lmd Load measurement duration
182      * @return byte[]
183      */
getHSWanMetricsPayload(int status, boolean symmetric, boolean capped, long downlinkSpeed, long uplinkSpeed, int downlinkLoad, int uplinkLoad, int lmd)184     private static byte[] getHSWanMetricsPayload(int status, boolean symmetric, boolean capped,
185             long downlinkSpeed, long uplinkSpeed, int downlinkLoad, int uplinkLoad, int lmd) {
186         ByteBuffer buffer = ByteBuffer.allocate(HSWanMetricsElement.EXPECTED_BUFFER_SIZE)
187                 .order(ByteOrder.LITTLE_ENDIAN);
188         int wanInfo = status & HSWanMetricsElement.LINK_STATUS_MASK;
189         if (symmetric) wanInfo |= HSWanMetricsElement.SYMMETRIC_LINK_MASK;
190         if (capped) wanInfo |= HSWanMetricsElement.AT_CAPACITY_MASK;
191         buffer.put((byte) wanInfo);
192         buffer.putInt((int) (downlinkSpeed & 0xFFFFFFFFL));
193         buffer.putInt((int) (uplinkSpeed & 0xFFFFFFFFL));
194         buffer.put((byte) (downlinkLoad & 0xFF));
195         buffer.put((byte) (uplinkLoad & 0xFF));
196         buffer.putShort((short) (lmd & 0xFFFF));
197         buffer.position(0);
198         byte[] data = new byte[HSWanMetricsElement.EXPECTED_BUFFER_SIZE];
199         buffer.get(data);
200         return data;
201     }
202 
203     /**
204      * Helper function for generating payload for a Hotspot 2.0 Connection Capability ANQP
205      * element.
206      *
207      * @param protocol Network protocol
208      * @param port Network port
209      * @param status Status of the port
210      * @return byte[]
211      */
getHSConnectionCapabilityPayload(int protocol, int port, int status)212     private static byte[] getHSConnectionCapabilityPayload(int protocol, int port, int status) {
213         ByteArrayOutputStream stream = new ByteArrayOutputStream();
214         stream.write((byte) protocol);
215         // Write 2-byte port in little-endian.
216         stream.write((byte) (port & 0xFF));
217         stream.write((byte) ((port >> 8) & 0xFF));
218         stream.write((byte) status);
219         return stream.toByteArray();
220     }
221 
222     /**
223      * Helper function for generating payload for a Hotspot 2.0 OSU Providers List ANQP
224      * element.
225      *
226      * @param osuSsidBytes Bytes of OSU SSID
227      * @return byte[]
228      */
getHSOsuProvidersPayload(byte[] osuSsidBytes)229     private static byte[] getHSOsuProvidersPayload(byte[] osuSsidBytes) throws IOException {
230         ByteArrayOutputStream out = new ByteArrayOutputStream();
231         out.write((byte) osuSsidBytes.length);
232         out.write(osuSsidBytes);
233         out.write((byte) 1);
234         out.write(OsuProviderInfoTestUtil.TEST_OSU_PROVIDER_INFO_RAW_BYTES);
235         return out.toByteArray();
236     }
237 
238     /**
239      * Helper function for generating payload for a list of I18Name.
240      *
241      * @param language Array of language
242      * @param text Array of text
243      * @return byte[]
244      * @throws IOException
245      */
getI18NameListPayload(String[] language, String[] text)246     private static byte[] getI18NameListPayload(String[] language, String[] text)
247             throws IOException {
248         ByteArrayOutputStream stream = new ByteArrayOutputStream();
249         for (int i = 0; i < language.length; i++) {
250             byte[] textBytes = text[i].getBytes(StandardCharsets.UTF_8);
251             int length = I18Name.LANGUAGE_CODE_LENGTH + text[i].length();
252             stream.write((byte) length);
253             stream.write(language[i].getBytes(StandardCharsets.US_ASCII));
254             // Add padding for two-character language code.
255             if (language[i].getBytes(StandardCharsets.US_ASCII).length
256                     < I18Name.LANGUAGE_CODE_LENGTH) {
257                 stream.write(new byte[]{(byte) 0x0});
258             }
259             stream.write(textBytes);
260         }
261         return stream.toByteArray();
262     }
263 
264     /**
265      * Verify that an expected VenueNameElement will be returned when parsing a buffer that
266      * contained a Venue Name ANQP element.
267      *
268      * @throws Exception
269      */
270     @Test
parseVenueNameElement()271     public void parseVenueNameElement() throws Exception {
272         // Test data.
273         String[] language = new String[] {"en"};
274         String[] text = new String[] {"test"};
275 
276         // Setup expectation.
277         List<I18Name> nameList = new ArrayList<>();
278         nameList.add(new I18Name(language[0], Locale.forLanguageTag(language[0]), text[0]));
279         VenueNameElement expected = new VenueNameElement(nameList);
280 
281         ByteBuffer buffer = ByteBuffer.wrap(getVenueNamePayload(language, text));
282         assertEquals(expected,
283                 ANQPParser.parseElement(Constants.ANQPElementType.ANQPVenueName, buffer));
284     }
285 
286     /**
287      * Verify that an expected IPAddressTypeAvailabilityElement will be returned when parsing a
288      * buffer that contained an IP Address Type Availability ANQP element.
289      *
290      * @throws Exception
291      */
292     @Test
parseIPAddressTypeAvailabilityElement()293     public void parseIPAddressTypeAvailabilityElement() throws Exception {
294         // Test data.
295         int ipAddressAvailability = IPAddressTypeAvailabilityElement.IPV4_PUBLIC << 2
296                 | IPAddressTypeAvailabilityElement.IPV6_AVAILABLE;
297 
298         // Setup expectation.
299         IPAddressTypeAvailabilityElement expected = new IPAddressTypeAvailabilityElement(
300                 IPAddressTypeAvailabilityElement.IPV4_PUBLIC,
301                 IPAddressTypeAvailabilityElement.IPV6_AVAILABLE);
302 
303         ByteBuffer buffer = ByteBuffer.wrap(new byte[] {(byte) ipAddressAvailability});
304         assertEquals(expected,
305                 ANQPParser.parseElement(Constants.ANQPElementType.ANQPIPAddrAvailability, buffer));
306     }
307 
308     /**
309      * Verify that an expected DomainNameElement will be returned when parsing a buffer that
310      * contained a Domain Name ANQP element.
311      *
312      * @throws Exception
313      */
314     @Test
parseDomainNameElement()315     public void parseDomainNameElement() throws Exception {
316         String[] testNames = new String[] {"test.com", "abc.com"};
317         DomainNameElement expected = new DomainNameElement(Arrays.asList(testNames));
318 
319         ByteBuffer buffer = ByteBuffer.wrap(getDomainNamePayload(testNames));
320         assertEquals(expected,
321                 ANQPParser.parseElement(Constants.ANQPElementType.ANQPDomName, buffer));
322     }
323 
324     /**
325      * Verify that an expected RoamingConsortiumElement will be returned when parsing a buffer that
326      * contained a Roaming Consortium ANQP element.
327      *
328      * @throws Exception
329      */
330     @Test
parseRoamingConsortium()331     public void parseRoamingConsortium() throws Exception {
332         Long[] ois = new Long[] {0x12345678L, 0x5678L};
333         int[] oisLength = new int[] {4, 2};
334         RoamingConsortiumElement expected = new RoamingConsortiumElement(Arrays.asList(ois));
335 
336         ByteBuffer buffer = ByteBuffer.wrap(getRoamingConsortiumPayload(ois, oisLength));
337         assertEquals(expected,
338                 ANQPParser.parseElement(Constants.ANQPElementType.ANQPRoamingConsortium, buffer));
339     }
340 
341     /**
342      * Verify that an expected NAIRealmElement will be returned when parsing a buffer that
343      * contained a NAI Realm ANQP element.
344      *
345      * @throws Exception
346      */
347     @Test
parseNAIRealmElement()348     public void parseNAIRealmElement() throws Exception {
349         byte[][] testBytes = new byte[][] {NAIRealmDataTestUtil.TEST_REAML_WITH_UTF8_DATA_BYTES};
350         NAIRealmData[] realmDataList = new NAIRealmData[] {NAIRealmDataTestUtil.TEST_REALM_DATA};
351         NAIRealmElement expected = new NAIRealmElement(Arrays.asList(realmDataList));
352 
353         ByteBuffer buffer = ByteBuffer.wrap(getNAIRealmPayload(testBytes));
354         assertEquals(expected,
355                 ANQPParser.parseElement(Constants.ANQPElementType.ANQPNAIRealm, buffer));
356     }
357 
358     /**
359      * Verify that an expected ThreeGPPNetworkElement will be returned when parsing a buffer that
360      * contained a 3GPP Network ANQP element.
361      *
362      * @throws Exception
363      */
364     @Test
parseThreeGPPNetworkElement()365     public void parseThreeGPPNetworkElement() throws Exception {
366         byte[][] plmnBytes = new byte[][] {new byte[] {(byte) 0x87, 0x29, 0x10}};
367         String[] plmnList = new String[] {"789012"};
368 
369         List<CellularNetwork> networkList = new ArrayList<>();
370         networkList.add(new CellularNetwork(Arrays.asList(plmnList)));
371         ThreeGPPNetworkElement expected = new ThreeGPPNetworkElement(networkList);
372 
373         ByteBuffer buffer = ByteBuffer.wrap(getThreeGPPNetworkPayload(
374                 new byte[][] {CellularNetworkTestUtil.formatPLMNListIEI(plmnBytes)}));
375         assertEquals(expected,
376                 ANQPParser.parseElement(Constants.ANQPElementType.ANQP3GPPNetwork, buffer));
377     }
378 
379     /**
380      * Verify that ProtocolException will be thrown when parsing a buffer that contained a
381      * vendor specific element that contained a non-Hotspot 2.0 ANQP-element.
382      *
383      * @throws Exception
384      */
385     @Test(expected = ProtocolException.class)
parseNonHS20VendorSpecificElement()386     public void parseNonHS20VendorSpecificElement() throws Exception {
387         ByteBuffer buffer = ByteBuffer.wrap(
388                 getVendorSpecificPayload(0x123456, 0x12, 1, new byte[0]));
389         ANQPParser.parseElement(Constants.ANQPElementType.ANQPVendorSpec, buffer);
390     }
391 
392     /**
393      * Verify that an expected HSFriendlyNameElement will be returned when parsing a buffer that
394      * contained a vendor specific element that contained a Hotspot 2.0 friendly name
395      * ANQP element.
396      *
397      * @throws Exception
398      */
399     @Test
parseVendorSpecificElementWithHSFriendlyName()400     public void parseVendorSpecificElementWithHSFriendlyName() throws Exception {
401         String[] language = new String[] {"en"};
402         String[] text = new String[] {"test"};
403 
404         // Setup expectation.
405         List<I18Name> nameList = new ArrayList<>();
406         nameList.add(new I18Name(language[0], Locale.forLanguageTag(language[0]), text[0]));
407         HSFriendlyNameElement expected = new HSFriendlyNameElement(nameList);
408 
409         byte[] hsFriendlyNameBytes = getHSFriendlyNamePayload(language, text);
410         byte[] data = getVendorSpecificPayload(
411                 ANQPParser.VENDOR_SPECIFIC_HS20_OI, ANQPParser.VENDOR_SPECIFIC_HS20_TYPE,
412                 Constants.HS_FRIENDLY_NAME, hsFriendlyNameBytes);
413         assertEquals(expected, ANQPParser.parseElement(
414                 Constants.ANQPElementType.ANQPVendorSpec, ByteBuffer.wrap(data)));
415     }
416 
417     /**
418      * Verify that an expected HSFriendlyNameElement will be returned when parsing a buffer that
419      * contained a Hotspot 2.0 Friendly Name ANQP element.
420      *
421      * @throws Exception
422      */
423     @Test
parseHSFrendlyNameElement()424     public void parseHSFrendlyNameElement() throws Exception {
425         // Test data.
426         String[] language = new String[] {"en"};
427         String[] text = new String[] {"test"};
428 
429         // Setup expectation.
430         List<I18Name> nameList = new ArrayList<>();
431         nameList.add(new I18Name(language[0], Locale.forLanguageTag(language[0]), text[0]));
432         HSFriendlyNameElement expected = new HSFriendlyNameElement(nameList);
433 
434         ByteBuffer buffer = ByteBuffer.wrap(getHSFriendlyNamePayload(language, text));
435         assertEquals(expected,
436                 ANQPParser.parseHS20Element(Constants.ANQPElementType.HSFriendlyName, buffer));
437     }
438 
439     /**
440      * Verify that an expected HSWanMetricsElement will be returned when parsing a buffer that
441      * contained a Hotspot 2.0 WAN Metrics ANQP element.
442      *
443      * @throws Exception
444      */
445     @Test
parseHSWANMetricsElement()446     public void parseHSWANMetricsElement() throws Exception {
447         int status = HSWanMetricsElement.LINK_STATUS_UP;
448         boolean symmetric = false;
449         boolean capped = true;
450         long downlinkSpeed = 0x12453L;
451         long uplinkSpeed = 0x12423L;
452         int downlinkLoad = 0x12;
453         int uplinkLoad = 0x23;
454         int lmd = 0x2321;
455 
456         HSWanMetricsElement expected = new HSWanMetricsElement(status, symmetric, capped,
457                 downlinkSpeed, uplinkSpeed, downlinkLoad, uplinkLoad, lmd);
458 
459         byte[] data = getHSWanMetricsPayload(status, symmetric, capped, downlinkSpeed,
460                 uplinkSpeed, downlinkLoad, uplinkLoad, lmd);
461         ByteBuffer buffer = ByteBuffer.wrap(data);
462         assertEquals(expected,
463                 ANQPParser.parseHS20Element(Constants.ANQPElementType.HSWANMetrics, buffer));
464     }
465 
466     /**
467      * Verify that an expected HSConnectionCapabilityElement will be returned when parsing a
468      * buffer that contained a Hotspot 2.0 Connection Capability ANQP element.
469      *
470      * @throws Exception
471      */
472     @Test
parseHSConnectionCapabilityElement()473     public void parseHSConnectionCapabilityElement() throws Exception {
474         int protocol = 12;
475         int port = 23;
476         int status = ProtocolPortTuple.PROTO_STATUS_OPEN;
477 
478         List<ProtocolPortTuple> statusList = new ArrayList<>();
479         statusList.add(new ProtocolPortTuple(protocol, port, status));
480         HSConnectionCapabilityElement expected = new HSConnectionCapabilityElement(statusList);
481 
482         ByteBuffer buffer = ByteBuffer.wrap(
483                 getHSConnectionCapabilityPayload(protocol, port, status));
484         assertEquals(expected,
485                 ANQPParser.parseHS20Element(Constants.ANQPElementType.HSConnCapability, buffer));
486     }
487 
488     /**
489      * Verify that an expected RawByteElement will be returned when parsing a buffer that
490      * contained a Hotspot 2.0 OSU Providers element.
491      *
492      * @throws Exception
493      */
494     @Test
parseHSOUSProvidersElement()495     public void parseHSOUSProvidersElement() throws Exception {
496         byte[] osuSsidBytes = "Test SSID".getBytes(StandardCharsets.UTF_8);
497         byte[] data = getHSOsuProvidersPayload(osuSsidBytes);
498 
499         HSOsuProvidersElement expected = new HSOsuProvidersElement(
500                 WifiSsid.fromBytes(osuSsidBytes),
501                 Arrays.asList(OsuProviderInfoTestUtil.TEST_OSU_PROVIDER_INFO));
502 
503         ByteBuffer buffer = ByteBuffer.wrap(data);
504         assertEquals(expected,
505                 ANQPParser.parseHS20Element(Constants.ANQPElementType.HSOSUProviders, buffer));
506     }
507 }
508