1 /*
2  * Copyright (C) 2021 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.google.android.iwlan.epdg;
18 
19 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
20 
21 import static com.google.android.iwlan.epdg.SrvDnsResolver.QUERY_TYPE_SRV;
22 
23 import static org.junit.Assert.assertEquals;
24 import static org.junit.Assert.assertNotNull;
25 import static org.mockito.ArgumentMatchers.any;
26 import static org.mockito.ArgumentMatchers.anyInt;
27 import static org.mockito.Mockito.doAnswer;
28 import static org.mockito.Mockito.lenient;
29 
30 import android.net.DnsResolver;
31 import android.net.Network;
32 import android.support.annotation.NonNull;
33 import android.support.annotation.Nullable;
34 import android.util.Log;
35 
36 import libcore.net.InetAddressUtils;
37 
38 import com.google.android.iwlan.epdg.SrvDnsResolver.SrvRecordInetAddress;
39 
40 import org.junit.After;
41 import org.junit.Before;
42 import org.junit.Test;
43 import org.mockito.ArgumentMatchers;
44 import org.mockito.Mock;
45 import org.mockito.MockitoAnnotations;
46 import org.mockito.MockitoSession;
47 
48 import java.net.InetAddress;
49 import java.net.UnknownHostException;
50 import java.util.Arrays;
51 import java.util.List;
52 import java.util.concurrent.CompletableFuture;
53 import java.util.concurrent.CompletionException;
54 import java.util.concurrent.ExecutionException;
55 import java.util.concurrent.Executor;
56 
57 public class SrvDnsResolverTest {
58     private static final String TAG = "SrvDnsResolverTest";
59 
60     private static final String TEST_QUERY = "_imaps._tcp.gmail.com";
61 
62     // SRV record response to TEST_QUERY. Reproduced with "dig _imaps._tcp.gmail.com -tSRV".
63     // This contains both the SRV record corresponding to the target name, as well as the IP
64     // addresses corresponding to the FQDN in the SRV record.
65     private static final byte[] TEST_QUERY_SRV_RESPONSE_IP_ADDRESSES = {
66         82, -42, -127, -128, 0, 1, 0, 1, 0, 0, 0, 4, 6, 95, 105, 109, 97, 112, 115, 4, 95, 116, 99,
67         112, 5, 103, 109, 97, 105, 108, 3, 99, 111, 109, 0, 0, 33, 0, 1, -64, 12, 0, 33, 0, 1, 0, 1,
68         81, -128, 0, 22, 0, 5, 0, 0, 3, -31, 4, 105, 109, 97, 112, 5, 103, 109, 97, 105, 108, 3, 99,
69         111, 109, 0, -64, 57, 0, 1, 0, 1, 0, 0, 0, 25, 0, 4, -114, -5, 2, 109, -64, 57, 0, 1, 0, 1,
70         0, 0, 0, 25, 0, 4, -114, -5, 2, 108, -64, 57, 0, 28, 0, 1, 0, 0, 0, 25, 0, 16, 38, 7, -8,
71         -80, 64, 35, 12, 3, 0, 0, 0, 0, 0, 0, 0, 109, -64, 57, 0, 28, 0, 1, 0, 0, 0, 25, 0, 16, 38,
72         7, -8, -80, 64, 35, 12, 3, 0, 0, 0, 0, 0, 0, 0, 108
73     };
74 
75     // SRV record response to TEST_QUERY, but on a different AP / DNS server, containing only
76     // the SRV record corresponding to the target name. Additional TYPE_A DNS lookups would be
77     // needed to pull the IP addresses corresponding to the target name.
78     private static final byte[] TEST_QUERY_SRV_RESPONSE = {
79         3, -109, -127, -128, 0, 1, 0, 1, 0, 0, 0, 0, 6, 95, 105, 109, 97, 112, 115, 4, 95, 116, 99,
80         112, 5, 103, 109, 97, 105, 108, 3, 99, 111, 109, 0, 0, 33, 0, 1, -64, 12, 0, 33, 0, 1, 0, 1,
81         80, -11, 0, 22, 0, 5, 0, 0, 3, -31, 4, 105, 109, 97, 112, 5, 103, 109, 97, 105, 108, 3, 99,
82         111, 109, 0
83     };
84 
85     // Response to the SRV query 'TEST_QUERY', with an unexpected record type in its answer (TYPE_A
86     // instead of QUERY_TYPE_SRV).
87     private static final byte[] TEST_QUERY_INVALID_SRV_RESPONSE = {
88         3, -109, -127, -128, 0, 1, 0, 1, 0, 0, 0, 0, 6, 95, 105, 109, 97, 112, 115, 4, 95, 116, 99,
89         112, 5, 103, 109, 97, 105, 108, 3, 99, 111, 109, 0, 0, 33, 0, 1, -64, 12, 0, 0, 0, 1, 0, 1,
90         80, -11, 0, 22, 0, 5, 0, 0, 3, -31, 4, 105, 109, 97, 112, 5, 103, 109, 97, 105, 108, 3, 99,
91         111, 109, 0
92     };
93 
94     // The IP addresses corresponding to the SRV record in TEST_QUERY_SRV_RESPONSE.
95     List<InetAddress> TEST_QUERY_RESPONSE_IP_ADDRESSES =
96             Arrays.asList(
97                     InetAddressUtils.parseNumericAddress("142.250.101.108"),
98                     InetAddressUtils.parseNumericAddress("142.250.101.109"),
99                     InetAddressUtils.parseNumericAddress("2607:f8b0:4023:c03::6d"),
100                     InetAddressUtils.parseNumericAddress("2607:f8b0:4023:c03::6c"));
101 
102     @Mock private Network mMockNetwork;
103     @Mock private DnsResolver mMockDnsResolver;
104 
105     MockitoSession mStaticMockSession;
106     final CompletableFuture<List<SrvRecordInetAddress>> mSrvDnsResult;
107     final DnsResolver.Callback<List<SrvRecordInetAddress>> mSrvDnsCb;
108 
SrvDnsResolverTest()109     public SrvDnsResolverTest() {
110         mSrvDnsResult = new CompletableFuture<>();
111         mSrvDnsCb =
112                 new DnsResolver.Callback<List<SrvRecordInetAddress>>() {
113                     @Override
114                     public void onAnswer(
115                             @NonNull final List<SrvRecordInetAddress> answer, final int rcode) {
116                         if (rcode == 0 && answer.size() != 0) {
117                             mSrvDnsResult.complete(answer);
118                         } else {
119                             mSrvDnsResult.completeExceptionally(new UnknownHostException());
120                         }
121                     }
122 
123                     @Override
124                     public void onError(@Nullable final DnsResolver.DnsException error) {
125                         mSrvDnsResult.completeExceptionally(error);
126                     }
127                 };
128     }
129 
130     @Before
setUp()131     public void setUp() throws Exception {
132         MockitoAnnotations.initMocks(this);
133         mStaticMockSession = mockitoSession().mockStatic(DnsResolver.class).startMocking();
134 
135         // lenient() here is used to mock the static method.
136         lenient().when(DnsResolver.getInstance()).thenReturn(mMockDnsResolver);
137     }
138 
139     @After
cleanUp()140     public void cleanUp() throws Exception {
141         mStaticMockSession.finishMocking();
142     }
143 
144     // Tests the case where the DNS server response includes both the SRV record and additionally,
145     // the IP address records corresponding to the FQDN in the SRV record.
146     @Test
testQueryGivesSrvAndIpAddressResponse()147     public void testQueryGivesSrvAndIpAddressResponse()
148             throws ExecutionException, InterruptedException {
149         doAnswer(
150                         invocation -> {
151                             final Executor executor = invocation.getArgument(5);
152                             final DnsResolver.Callback<byte[]> callback = invocation.getArgument(7);
153                             executor.execute(
154                                     () ->
155                                             callback.onAnswer(
156                                                     TEST_QUERY_SRV_RESPONSE_IP_ADDRESSES, 0));
157                             return null;
158                         })
159                 .when(mMockDnsResolver)
160                 .rawQuery(
161                         any(),
162                         ArgumentMatchers.eq(TEST_QUERY),
163                         ArgumentMatchers.eq(DnsResolver.CLASS_IN),
164                         ArgumentMatchers.eq(QUERY_TYPE_SRV),
165                         anyInt(),
166                         any(),
167                         any(),
168                         any());
169 
170         SrvDnsResolver.query(mMockNetwork, TEST_QUERY, Runnable::run, null, mSrvDnsCb);
171         final List<SrvRecordInetAddress> records = mSrvDnsResult.join();
172 
173         assertEquals(4, records.size());
174 
175         SrvRecordInetAddress record = records.get(0);
176         assertEquals("142.251.2.109", record.mInetAddress.getHostAddress());
177         assertEquals(993, record.mPort);
178 
179         record = records.get(1);
180         assertEquals("142.251.2.108", record.mInetAddress.getHostAddress());
181         assertEquals(993, record.mPort);
182 
183         record = records.get(2);
184         assertEquals("2607:f8b0:4023:c03::6d", record.mInetAddress.getHostAddress());
185         assertEquals(993, record.mPort);
186 
187         record = records.get(3);
188         assertEquals("2607:f8b0:4023:c03::6c", record.mInetAddress.getHostAddress());
189         assertEquals(993, record.mPort);
190     }
191 
192     // Tests the case where the DNS server's Type SRV response includes only the SRV record, and the
193     // corresponding TYPE_A/AAAA records a pulled with a second-level DNS query.
194     @Test
testQueryGivesSrvResponseFollowUpQueriesGiveIpAddress()195     public void testQueryGivesSrvResponseFollowUpQueriesGiveIpAddress()
196             throws ExecutionException, InterruptedException {
197         doAnswer(
198                         invocation -> {
199                             Executor executor = invocation.getArgument(5);
200                             DnsResolver.Callback<byte[]> callback = invocation.getArgument(7);
201                             executor.execute(() -> callback.onAnswer(TEST_QUERY_SRV_RESPONSE, 0));
202                             return null;
203                         })
204                 .when(mMockDnsResolver)
205                 .rawQuery(
206                         any(),
207                         ArgumentMatchers.eq(TEST_QUERY),
208                         ArgumentMatchers.eq(DnsResolver.CLASS_IN),
209                         ArgumentMatchers.eq(QUERY_TYPE_SRV),
210                         anyInt(),
211                         any(),
212                         any(),
213                         any());
214 
215         doAnswer(
216                         invocation -> {
217                             Executor executor = invocation.getArgument(3);
218                             DnsResolver.Callback<? super List<InetAddress>> callback =
219                                     invocation.getArgument(5);
220                             executor.execute(
221                                     () -> callback.onAnswer(TEST_QUERY_RESPONSE_IP_ADDRESSES, 0));
222                             return null;
223                         })
224                 .when(mMockDnsResolver)
225                 .query(
226                         any(),
227                         ArgumentMatchers.eq("imap.gmail.com"),
228                         ArgumentMatchers.eq(DnsResolver.FLAG_EMPTY),
229                         any(),
230                         any(),
231                         any());
232 
233         SrvDnsResolver.query(mMockNetwork, TEST_QUERY, Runnable::run, null, mSrvDnsCb);
234         List<SrvRecordInetAddress> records = mSrvDnsResult.join();
235         assertEquals(4, records.size());
236 
237         SrvRecordInetAddress record = records.get(0);
238         assertEquals("142.250.101.108", record.mInetAddress.getHostAddress());
239         assertEquals(993, record.mPort);
240 
241         record = records.get(1);
242         assertEquals("142.250.101.109", record.mInetAddress.getHostAddress());
243         assertEquals(993, record.mPort);
244 
245         record = records.get(2);
246         assertEquals("2607:f8b0:4023:c03::6d", record.mInetAddress.getHostAddress());
247         assertEquals(993, record.mPort);
248 
249         record = records.get(3);
250         assertEquals("2607:f8b0:4023:c03::6c", record.mInetAddress.getHostAddress());
251         assertEquals(993, record.mPort);
252     }
253 
254     // Tests the case where the DNS server response contains a TYPE_A record instead of a
255     // QUERY_TYPE_SRV record in the answer field, and the implementation throws a DnsException.
256     @Test
testInvalidResponseThrowsParseException()257     public void testInvalidResponseThrowsParseException()
258             throws ExecutionException, InterruptedException {
259         doAnswer(
260                         invocation -> {
261                             final Executor executor = invocation.getArgument(5);
262                             final DnsResolver.Callback<byte[]> callback = invocation.getArgument(7);
263                             executor.execute(
264                                     () -> callback.onAnswer(TEST_QUERY_INVALID_SRV_RESPONSE, 0));
265                             return null;
266                         })
267                 .when(mMockDnsResolver)
268                 .rawQuery(
269                         any(),
270                         ArgumentMatchers.eq(TEST_QUERY),
271                         ArgumentMatchers.eq(DnsResolver.CLASS_IN),
272                         ArgumentMatchers.eq(QUERY_TYPE_SRV),
273                         anyInt(),
274                         any(),
275                         any(),
276                         any());
277 
278         SrvDnsResolver.query(mMockNetwork, TEST_QUERY, Runnable::run, null, mSrvDnsCb);
279         DnsResolver.DnsException exception = null;
280         try {
281             mSrvDnsResult.join();
282         } catch (CompletionException e) {
283             exception = (DnsResolver.DnsException) e.getCause();
284             Log.d(TAG, e.getMessage() + e.getCause());
285         }
286         assertNotNull("Exception wasn't thrown!", exception);
287         assertEquals(DnsResolver.ERROR_PARSE, exception.code);
288     }
289 }
290