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