Snap for 6453963 from 28ff52e32efbff9fad525c95291007e13ad5fe76 to rvc-release
Change-Id: I26cbfacc4a5dc3f30f31d88914c2eb7c81e7075c
This commit is contained in:
@@ -30,6 +30,7 @@ import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isDozeModeSupp
|
||||
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.restrictBackgroundValueToString;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
@@ -177,7 +178,9 @@ public abstract class AbstractRestrictBackgroundNetworkTestCase {
|
||||
do {
|
||||
attempts++;
|
||||
count = getNumberBroadcastsReceived(receiverName, ACTION_RESTRICT_BACKGROUND_CHANGED);
|
||||
if (count >= expectedCount) {
|
||||
assertFalse("Expected count " + expectedCount + " but actual is " + count,
|
||||
count > expectedCount);
|
||||
if (count == expectedCount) {
|
||||
break;
|
||||
}
|
||||
Log.d(TAG, "Expecting count " + expectedCount + " but actual is " + count + " after "
|
||||
|
||||
@@ -47,6 +47,7 @@ java_defaults {
|
||||
"mockwebserver",
|
||||
"junit",
|
||||
"junit-params",
|
||||
"libnanohttpd",
|
||||
"truth-prebuilt",
|
||||
],
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ android_test {
|
||||
"androidx.test.ext.junit",
|
||||
"compatibility-device-util-axt",
|
||||
"ctstestrunner-axt",
|
||||
"net-tests-utils",
|
||||
],
|
||||
|
||||
platform_apis: true,
|
||||
|
||||
@@ -46,6 +46,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import com.android.internal.net.ipsec.ike.testutils.CertUtils;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -63,7 +64,7 @@ import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class IkeSessionParamsTest extends IkeSessionParamsTestBase {
|
||||
public final class IkeSessionParamsTest extends IkeSessionTestBase {
|
||||
private static final int HARD_LIFETIME_SECONDS = (int) TimeUnit.HOURS.toSeconds(20L);
|
||||
private static final int SOFT_LIFETIME_SECONDS = (int) TimeUnit.HOURS.toSeconds(10L);
|
||||
private static final int DPD_DELAY_SECONDS = (int) TimeUnit.MINUTES.toSeconds(10L);
|
||||
@@ -105,6 +106,9 @@ public final class IkeSessionParamsTest extends IkeSessionParamsTestBase {
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
// This address is never used except for setting up the test network
|
||||
setUpTestNetwork(IPV4_ADDRESS_LOCAL);
|
||||
|
||||
mServerCaCert = CertUtils.createCertFromPemFile("server-a-self-signed-ca.pem");
|
||||
mClientEndCert = CertUtils.createCertFromPemFile("client-a-end-cert.pem");
|
||||
mClientIntermediateCaCertOne =
|
||||
@@ -114,6 +118,11 @@ public final class IkeSessionParamsTest extends IkeSessionParamsTestBase {
|
||||
mClientPrivateKey = CertUtils.createRsaPrivateKeyFromKeyFile("client-a-private-key.key");
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
tearDownTestNetwork();
|
||||
}
|
||||
|
||||
private static EapSessionConfig.Builder createEapOnlySafeMethodsBuilder() {
|
||||
return new EapSessionConfig.Builder()
|
||||
.setEapIdentity(EAP_IDENTITY)
|
||||
@@ -131,7 +140,7 @@ public final class IkeSessionParamsTest extends IkeSessionParamsTestBase {
|
||||
*/
|
||||
private IkeSessionParams.Builder createIkeParamsBuilderMinimum() {
|
||||
return new IkeSessionParams.Builder(sContext)
|
||||
.setNetwork(sTunNetwork)
|
||||
.setNetwork(mTunNetwork)
|
||||
.setServerHostname(IPV4_ADDRESS_REMOTE.getHostAddress())
|
||||
.addSaProposal(SA_PROPOSAL)
|
||||
.setLocalIdentification(LOCAL_ID)
|
||||
@@ -145,7 +154,7 @@ public final class IkeSessionParamsTest extends IkeSessionParamsTestBase {
|
||||
* @see #createIkeParamsBuilderMinimum
|
||||
*/
|
||||
private void verifyIkeParamsMinimum(IkeSessionParams sessionParams) {
|
||||
assertEquals(sTunNetwork, sessionParams.getNetwork());
|
||||
assertEquals(mTunNetwork, sessionParams.getNetwork());
|
||||
assertEquals(IPV4_ADDRESS_REMOTE.getHostAddress(), sessionParams.getServerHostname());
|
||||
assertEquals(Arrays.asList(SA_PROPOSAL), sessionParams.getSaProposals());
|
||||
assertEquals(LOCAL_ID, sessionParams.getLocalIdentification());
|
||||
@@ -268,7 +277,7 @@ public final class IkeSessionParamsTest extends IkeSessionParamsTestBase {
|
||||
*/
|
||||
private IkeSessionParams.Builder createIkeParamsBuilderMinimumWithoutAuth() {
|
||||
return new IkeSessionParams.Builder(sContext)
|
||||
.setNetwork(sTunNetwork)
|
||||
.setNetwork(mTunNetwork)
|
||||
.setServerHostname(IPV4_ADDRESS_REMOTE.getHostAddress())
|
||||
.addSaProposal(SA_PROPOSAL)
|
||||
.setLocalIdentification(LOCAL_ID)
|
||||
@@ -282,7 +291,7 @@ public final class IkeSessionParamsTest extends IkeSessionParamsTestBase {
|
||||
* @see #createIkeParamsBuilderMinimumWithoutAuth
|
||||
*/
|
||||
private void verifyIkeParamsMinimumWithoutAuth(IkeSessionParams sessionParams) {
|
||||
assertEquals(sTunNetwork, sessionParams.getNetwork());
|
||||
assertEquals(mTunNetwork, sessionParams.getNetwork());
|
||||
assertEquals(IPV4_ADDRESS_REMOTE.getHostAddress(), sessionParams.getServerHostname());
|
||||
assertEquals(Arrays.asList(SA_PROPOSAL), sessionParams.getSaProposals());
|
||||
assertEquals(LOCAL_ID, sessionParams.getLocalIdentification());
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.net.ipsec.ike.cts;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.LinkAddress;
|
||||
import android.net.Network;
|
||||
import android.net.TestNetworkInterface;
|
||||
import android.net.TestNetworkManager;
|
||||
import android.net.ipsec.ike.cts.TestNetworkUtils.TestNetworkCallback;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.platform.test.annotations.AppModeFull;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@AppModeFull(reason = "MANAGE_TEST_NETWORKS permission can't be granted to instant apps")
|
||||
abstract class IkeSessionParamsTestBase extends IkeTestBase {
|
||||
// Static state to reduce setup/teardown
|
||||
static ConnectivityManager sCM;
|
||||
static TestNetworkManager sTNM;
|
||||
static ParcelFileDescriptor sTunFd;
|
||||
static TestNetworkCallback sTunNetworkCallback;
|
||||
static Network sTunNetwork;
|
||||
|
||||
static Context sContext = InstrumentationRegistry.getContext();
|
||||
static IBinder sBinder = new Binder();
|
||||
|
||||
// This method is guaranteed to run in subclasses and will run before subclasses' @BeforeClass
|
||||
// methods.
|
||||
@BeforeClass
|
||||
public static void setUpTestNetworkBeforeClass() throws Exception {
|
||||
InstrumentationRegistry.getInstrumentation()
|
||||
.getUiAutomation()
|
||||
.adoptShellPermissionIdentity();
|
||||
sCM = (ConnectivityManager) sContext.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
sTNM = (TestNetworkManager) sContext.getSystemService(Context.TEST_NETWORK_SERVICE);
|
||||
|
||||
TestNetworkInterface testIface =
|
||||
sTNM.createTunInterface(
|
||||
new LinkAddress[] {new LinkAddress(IPV4_ADDRESS_LOCAL, IP4_PREFIX_LEN)});
|
||||
|
||||
sTunFd = testIface.getFileDescriptor();
|
||||
sTunNetworkCallback =
|
||||
TestNetworkUtils.setupAndGetTestNetwork(
|
||||
sCM, sTNM, testIface.getInterfaceName(), sBinder);
|
||||
sTunNetwork = sTunNetworkCallback.getNetworkBlocking();
|
||||
}
|
||||
|
||||
// This method is guaranteed to run in subclasses and will run after subclasses' @AfterClass
|
||||
// methods.
|
||||
@AfterClass
|
||||
public static void tearDownTestNetworkAfterClass() throws Exception {
|
||||
sCM.unregisterNetworkCallback(sTunNetworkCallback);
|
||||
|
||||
sTNM.teardownTestNetwork(sTunNetwork);
|
||||
sTunFd.close();
|
||||
|
||||
InstrumentationRegistry.getInstrumentation()
|
||||
.getUiAutomation()
|
||||
.dropShellPermissionIdentity();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.net.ipsec.ike.cts;
|
||||
|
||||
import static android.net.ipsec.ike.IkeSessionConfiguration.EXTENSION_TYPE_FRAGMENTATION;
|
||||
import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_NO_PROPOSAL_CHOSEN;
|
||||
import static android.system.OsConstants.AF_INET;
|
||||
import static android.system.OsConstants.AF_INET6;
|
||||
|
||||
import static com.android.internal.util.HexDump.hexStringToByteArray;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.net.ipsec.ike.ChildSessionConfiguration;
|
||||
import android.net.ipsec.ike.IkeFqdnIdentification;
|
||||
import android.net.ipsec.ike.IkeSession;
|
||||
import android.net.ipsec.ike.IkeSessionConfiguration;
|
||||
import android.net.ipsec.ike.IkeSessionConnectionInfo;
|
||||
import android.net.ipsec.ike.IkeSessionParams;
|
||||
import android.net.ipsec.ike.TunnelModeChildSessionParams;
|
||||
import android.net.ipsec.ike.exceptions.IkeException;
|
||||
import android.net.ipsec.ike.exceptions.IkeProtocolException;
|
||||
import android.platform.test.annotations.AppModeFull;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Arrays;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@AppModeFull(reason = "MANAGE_TEST_NETWORKS permission can't be granted to instant apps")
|
||||
public class IkeSessionPskTest extends IkeSessionTestBase {
|
||||
// Test vectors for success workflow
|
||||
private static final String SUCCESS_IKE_INIT_RESP =
|
||||
"46B8ECA1E0D72A18B45427679F9245D421202220000000000000015022000030"
|
||||
+ "0000002C010100040300000C0100000C800E0080030000080300000203000008"
|
||||
+ "0200000200000008040000022800008800020000A7AA3435D088EC1A2B7C2A47"
|
||||
+ "1FA1B85F1066C9B2006E7C353FB5B5FDBC2A88347ED2C6F5B7A265D03AE34039"
|
||||
+ "6AAC0145CFCC93F8BDB219DDFF22A603B8856A5DC59B6FAB7F17C5660CF38670"
|
||||
+ "8794FC72F273ADEB7A4F316519794AED6F8AB61F95DFB360FAF18C6C8CABE471"
|
||||
+ "6E18FE215348C2E582171A57FC41146B16C4AFE429000024A634B61C0E5C90C6"
|
||||
+ "8D8818B0955B125A9B1DF47BBD18775710792E651083105C2900001C00004004"
|
||||
+ "406FA3C5685A16B9B72C7F2EEE9993462C619ABE2900001C00004005AF905A87"
|
||||
+ "0A32222AA284A7070585601208A282F0290000080000402E290000100000402F"
|
||||
+ "00020003000400050000000800004014";
|
||||
private static final String SUCCESS_IKE_AUTH_RESP =
|
||||
"46B8ECA1E0D72A18B45427679F9245D42E20232000000001000000EC240000D0"
|
||||
+ "0D06D37198F3F0962DE8170D66F1A9008267F98CDD956D984BDCED2FC7FAF84A"
|
||||
+ "A6664EF25049B46B93C9ED420488E0C172AA6635BF4011C49792EF2B88FE7190"
|
||||
+ "E8859FEEF51724FD20C46E7B9A9C3DC4708EF7005707A18AB747C903ABCEAC5C"
|
||||
+ "6ECF5A5FC13633DCE3844A920ED10EF202F115DBFBB5D6D2D7AB1F34EB08DE7C"
|
||||
+ "A54DCE0A3A582753345CA2D05A0EFDB9DC61E81B2483B7D13EEE0A815D37252C"
|
||||
+ "23D2F29E9C30658227D2BB0C9E1A481EAA80BC6BE9006BEDC13E925A755A0290"
|
||||
+ "AEC4164D29997F52ED7DCC2E";
|
||||
private static final String SUCCESS_CREATE_CHILD_RESP =
|
||||
"46B8ECA1E0D72A18B45427679F9245D42E20242000000002000000CC210000B0"
|
||||
+ "484565D4AF6546274674A8DE339E9C9584EE2326AB9260F41C4D0B6C5B02D1D"
|
||||
+ "2E8394E3CDE3094895F2ACCABCDCA8E82960E5196E9622BD13745FC8D6A2BED"
|
||||
+ "E561FF5D9975421BC463C959A3CBA3478256B6D278159D99B512DDF56AC1658"
|
||||
+ "63C65A986F395FE8B1476124B91F83FD7865304EB95B22CA4DD9601DA7A2533"
|
||||
+ "ABF4B36EB1B8CD09522F6A600032316C74E562E6756D9D49D945854E2ABDC4C"
|
||||
+ "3AF36305353D60D40B58BE44ABF82";
|
||||
private static final String SUCCESS_DELETE_CHILD_RESP =
|
||||
"46B8ECA1E0D72A18B45427679F9245D42E202520000000030000004C2A000030"
|
||||
+ "0C5CEB882DBCA65CE32F4C53909335F1365C91C555316C5E9D9FB553F7AA916"
|
||||
+ "EF3A1D93460B7FABAF0B4B854";
|
||||
private static final String SUCCESS_DELETE_IKE_RESP =
|
||||
"46B8ECA1E0D72A18B45427679F9245D42E202520000000040000004C00000030"
|
||||
+ "9352D71100777B00ABCC6BD7DBEA697827FFAAA48DF9A54D1D68161939F5DC8"
|
||||
+ "6743A7CEB2BE34AC00095A5B8";
|
||||
|
||||
private static final long IKE_INIT_SPI = Long.parseLong("46B8ECA1E0D72A18", 16);
|
||||
|
||||
private static final TunnelModeChildSessionParams CHILD_PARAMS =
|
||||
new TunnelModeChildSessionParams.Builder()
|
||||
.addSaProposal(SaProposalTest.buildChildSaProposalWithNormalModeCipher())
|
||||
.addSaProposal(SaProposalTest.buildChildSaProposalWithCombinedModeCipher())
|
||||
.addInternalAddressRequest(AF_INET)
|
||||
.addInternalAddressRequest(AF_INET6)
|
||||
.build();
|
||||
|
||||
private IkeSessionParams createIkeSessionParams(InetAddress mRemoteAddress) {
|
||||
return new IkeSessionParams.Builder(sContext)
|
||||
.setNetwork(mTunNetwork)
|
||||
.setServerHostname(mRemoteAddress.getHostAddress())
|
||||
.addSaProposal(SaProposalTest.buildIkeSaProposalWithNormalModeCipher())
|
||||
.addSaProposal(SaProposalTest.buildIkeSaProposalWithCombinedModeCipher())
|
||||
.setLocalIdentification(new IkeFqdnIdentification(LOCAL_HOSTNAME))
|
||||
.setRemoteIdentification(new IkeFqdnIdentification(REMOTE_HOSTNAME))
|
||||
.setAuthPsk(IKE_PSK)
|
||||
.build();
|
||||
}
|
||||
|
||||
private IkeSession openIkeSession(IkeSessionParams ikeParams) {
|
||||
return new IkeSession(
|
||||
sContext,
|
||||
ikeParams,
|
||||
CHILD_PARAMS,
|
||||
mUserCbExecutor,
|
||||
mIkeSessionCallback,
|
||||
mFirstChildSessionCallback);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIkeSessionSetupAndManageChildSas() throws Exception {
|
||||
// Open IKE Session
|
||||
IkeSession ikeSession = openIkeSession(createIkeSessionParams(mRemoteAddress));
|
||||
int expectedMsgId = 0;
|
||||
mTunUtils.awaitReqAndInjectResp(
|
||||
IKE_INIT_SPI,
|
||||
expectedMsgId++,
|
||||
false /* expectedUseEncap */,
|
||||
hexStringToByteArray(SUCCESS_IKE_INIT_RESP));
|
||||
|
||||
mTunUtils.awaitReqAndInjectResp(
|
||||
IKE_INIT_SPI,
|
||||
expectedMsgId++,
|
||||
true /* expectedUseEncap */,
|
||||
hexStringToByteArray(SUCCESS_IKE_AUTH_RESP));
|
||||
|
||||
// Verify opening IKE Session
|
||||
IkeSessionConfiguration ikeConfig = mIkeSessionCallback.awaitIkeConfig();
|
||||
assertNotNull(ikeConfig);
|
||||
assertEquals(EXPECTED_REMOTE_APP_VERSION_EMPTY, ikeConfig.getRemoteApplicationVersion());
|
||||
assertTrue(ikeConfig.getRemoteVendorIds().isEmpty());
|
||||
assertTrue(ikeConfig.getPcscfServers().isEmpty());
|
||||
assertTrue(ikeConfig.isIkeExtensionEnabled(EXTENSION_TYPE_FRAGMENTATION));
|
||||
|
||||
IkeSessionConnectionInfo ikeConnectInfo = ikeConfig.getIkeSessionConnectionInfo();
|
||||
assertNotNull(ikeConnectInfo);
|
||||
assertEquals(mLocalAddress, ikeConnectInfo.getLocalAddress());
|
||||
assertEquals(mRemoteAddress, ikeConnectInfo.getRemoteAddress());
|
||||
assertEquals(mTunNetwork, ikeConnectInfo.getNetwork());
|
||||
|
||||
// Verify opening first Child Session
|
||||
ChildSessionConfiguration firstChildConfig = mFirstChildSessionCallback.awaitChildConfig();
|
||||
assertNotNull(firstChildConfig);
|
||||
assertEquals(
|
||||
Arrays.asList(EXPECTED_INBOUND_TS), firstChildConfig.getInboundTrafficSelectors());
|
||||
assertEquals(Arrays.asList(DEFAULT_V4_TS), firstChildConfig.getOutboundTrafficSelectors());
|
||||
assertEquals(
|
||||
Arrays.asList(EXPECTED_INTERNAL_LINK_ADDR),
|
||||
firstChildConfig.getInternalAddresses());
|
||||
assertTrue(firstChildConfig.getInternalSubnets().isEmpty());
|
||||
assertTrue(firstChildConfig.getInternalDnsServers().isEmpty());
|
||||
assertTrue(firstChildConfig.getInternalDhcpServers().isEmpty());
|
||||
|
||||
// Open additional Child Session
|
||||
TestChildSessionCallback additionalChildCb = new TestChildSessionCallback();
|
||||
ikeSession.openChildSession(CHILD_PARAMS, additionalChildCb);
|
||||
mTunUtils.awaitReqAndInjectResp(
|
||||
IKE_INIT_SPI,
|
||||
expectedMsgId++,
|
||||
true /* expectedUseEncap */,
|
||||
hexStringToByteArray(SUCCESS_CREATE_CHILD_RESP));
|
||||
|
||||
// Verify opening additional Child Session
|
||||
ChildSessionConfiguration additionalChildConfig = additionalChildCb.awaitChildConfig();
|
||||
assertNotNull(additionalChildConfig);
|
||||
assertEquals(
|
||||
Arrays.asList(EXPECTED_INBOUND_TS), firstChildConfig.getInboundTrafficSelectors());
|
||||
assertEquals(Arrays.asList(DEFAULT_V4_TS), firstChildConfig.getOutboundTrafficSelectors());
|
||||
assertTrue(additionalChildConfig.getInternalAddresses().isEmpty());
|
||||
assertTrue(firstChildConfig.getInternalSubnets().isEmpty());
|
||||
assertTrue(firstChildConfig.getInternalDnsServers().isEmpty());
|
||||
assertTrue(firstChildConfig.getInternalDhcpServers().isEmpty());
|
||||
|
||||
// Close additional Child Session
|
||||
ikeSession.closeChildSession(additionalChildCb);
|
||||
mTunUtils.awaitReqAndInjectResp(
|
||||
IKE_INIT_SPI,
|
||||
expectedMsgId++,
|
||||
true /* expectedUseEncap */,
|
||||
hexStringToByteArray(SUCCESS_DELETE_CHILD_RESP));
|
||||
|
||||
additionalChildCb.awaitOnClosed();
|
||||
|
||||
// Close IKE Session
|
||||
ikeSession.close();
|
||||
mTunUtils.awaitReqAndInjectResp(
|
||||
IKE_INIT_SPI,
|
||||
expectedMsgId++,
|
||||
true /* expectedUseEncap */,
|
||||
hexStringToByteArray(SUCCESS_DELETE_IKE_RESP));
|
||||
|
||||
mFirstChildSessionCallback.awaitOnClosed();
|
||||
mIkeSessionCallback.awaitOnClosed();
|
||||
|
||||
// TODO: verify IpSecTransform pair is created and deleted
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIkeSessionKill() throws Exception {
|
||||
// Open IKE Session
|
||||
IkeSession ikeSession = openIkeSession(createIkeSessionParams(mRemoteAddress));
|
||||
int expectedMsgId = 0;
|
||||
mTunUtils.awaitReqAndInjectResp(
|
||||
IKE_INIT_SPI,
|
||||
expectedMsgId++,
|
||||
false /* expectedUseEncap */,
|
||||
hexStringToByteArray(SUCCESS_IKE_INIT_RESP));
|
||||
|
||||
mTunUtils.awaitReqAndInjectResp(
|
||||
IKE_INIT_SPI,
|
||||
expectedMsgId++,
|
||||
true /* expectedUseEncap */,
|
||||
hexStringToByteArray(SUCCESS_IKE_AUTH_RESP));
|
||||
|
||||
ikeSession.kill();
|
||||
|
||||
mFirstChildSessionCallback.awaitOnClosed();
|
||||
mIkeSessionCallback.awaitOnClosed();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIkeInitFail() throws Exception {
|
||||
String ikeInitFailRespHex =
|
||||
"46B8ECA1E0D72A180000000000000000292022200000000000000024000000080000000E";
|
||||
|
||||
// Open IKE Session
|
||||
IkeSession ikeSession = openIkeSession(createIkeSessionParams(mRemoteAddress));
|
||||
int expectedMsgId = 0;
|
||||
mTunUtils.awaitReqAndInjectResp(
|
||||
IKE_INIT_SPI,
|
||||
expectedMsgId++,
|
||||
false /* expectedUseEncap */,
|
||||
hexStringToByteArray(ikeInitFailRespHex));
|
||||
|
||||
IkeException exception = mIkeSessionCallback.awaitOnClosedException();
|
||||
assertNotNull(exception);
|
||||
assertTrue(exception instanceof IkeProtocolException);
|
||||
IkeProtocolException protocolException = (IkeProtocolException) exception;
|
||||
assertEquals(ERROR_TYPE_NO_PROPOSAL_CHOSEN, protocolException.getErrorType());
|
||||
assertArrayEquals(EXPECTED_PROTOCOL_ERROR_DATA_NONE, protocolException.getErrorData());
|
||||
}
|
||||
|
||||
// TODO(b/148689509): Verify rekey process and handling IKE_AUTH failure
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.net.ipsec.ike.cts;
|
||||
|
||||
import static android.app.AppOpsManager.OP_MANAGE_IPSEC_TUNNELS;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.app.AppOpsManager;
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.InetAddresses;
|
||||
import android.net.IpSecTransform;
|
||||
import android.net.LinkAddress;
|
||||
import android.net.Network;
|
||||
import android.net.TestNetworkInterface;
|
||||
import android.net.TestNetworkManager;
|
||||
import android.net.annotations.PolicyDirection;
|
||||
import android.net.ipsec.ike.ChildSessionCallback;
|
||||
import android.net.ipsec.ike.ChildSessionConfiguration;
|
||||
import android.net.ipsec.ike.IkeSessionCallback;
|
||||
import android.net.ipsec.ike.IkeSessionConfiguration;
|
||||
import android.net.ipsec.ike.IkeTrafficSelector;
|
||||
import android.net.ipsec.ike.cts.TestNetworkUtils.TestNetworkCallback;
|
||||
import android.net.ipsec.ike.exceptions.IkeException;
|
||||
import android.net.ipsec.ike.exceptions.IkeProtocolException;
|
||||
import android.os.Binder;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.platform.test.annotations.AppModeFull;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import com.android.compatibility.common.util.SystemUtil;
|
||||
import com.android.testutils.ArrayTrackRecord;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Package private base class for testing IkeSessionParams and IKE exchanges.
|
||||
*
|
||||
* <p>Subclasses MUST explicitly call #setUpTestNetwork and #tearDownTestNetwork to be able to use
|
||||
* the test network
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@AppModeFull(reason = "MANAGE_TEST_NETWORKS permission can't be granted to instant apps")
|
||||
abstract class IkeSessionTestBase extends IkeTestBase {
|
||||
// Package-wide common expected results that will be shared by all IKE/Child SA creation tests
|
||||
static final String EXPECTED_REMOTE_APP_VERSION_EMPTY = "";
|
||||
static final byte[] EXPECTED_PROTOCOL_ERROR_DATA_NONE = new byte[0];
|
||||
static final InetAddress EXPECTED_INTERNAL_ADDR =
|
||||
InetAddresses.parseNumericAddress("198.51.100.10");
|
||||
static final LinkAddress EXPECTED_INTERNAL_LINK_ADDR =
|
||||
new LinkAddress(EXPECTED_INTERNAL_ADDR, IP4_PREFIX_LEN);
|
||||
static final IkeTrafficSelector EXPECTED_INBOUND_TS =
|
||||
new IkeTrafficSelector(
|
||||
MIN_PORT, MAX_PORT, EXPECTED_INTERNAL_ADDR, EXPECTED_INTERNAL_ADDR);
|
||||
|
||||
// Static state to reduce setup/teardown
|
||||
static Context sContext = InstrumentationRegistry.getContext();
|
||||
static ConnectivityManager sCM =
|
||||
(ConnectivityManager) sContext.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
static TestNetworkManager sTNM;
|
||||
|
||||
private static final int TIMEOUT_MS = 500;
|
||||
|
||||
// Constants to be used for providing different IP addresses for each tests
|
||||
private static final byte IP_ADDR_LAST_BYTE_MAX = (byte) 100;
|
||||
private static final byte[] INITIAL_AVAILABLE_IP4_ADDR_LOCAL =
|
||||
InetAddresses.parseNumericAddress("192.0.2.1").getAddress();
|
||||
private static final byte[] INITIAL_AVAILABLE_IP4_ADDR_REMOTE =
|
||||
InetAddresses.parseNumericAddress("198.51.100.1").getAddress();
|
||||
private static final byte[] NEXT_AVAILABLE_IP4_ADDR_LOCAL = INITIAL_AVAILABLE_IP4_ADDR_LOCAL;
|
||||
private static final byte[] NEXT_AVAILABLE_IP4_ADDR_REMOTE = INITIAL_AVAILABLE_IP4_ADDR_REMOTE;
|
||||
|
||||
ParcelFileDescriptor mTunFd;
|
||||
TestNetworkCallback mTunNetworkCallback;
|
||||
Network mTunNetwork;
|
||||
IkeTunUtils mTunUtils;
|
||||
|
||||
InetAddress mLocalAddress;
|
||||
InetAddress mRemoteAddress;
|
||||
|
||||
Executor mUserCbExecutor;
|
||||
TestIkeSessionCallback mIkeSessionCallback;
|
||||
TestChildSessionCallback mFirstChildSessionCallback;
|
||||
|
||||
// This method is guaranteed to run in subclasses and will run before subclasses' @BeforeClass
|
||||
// methods.
|
||||
@BeforeClass
|
||||
public static void setUpPermissionBeforeClass() throws Exception {
|
||||
InstrumentationRegistry.getInstrumentation()
|
||||
.getUiAutomation()
|
||||
.adoptShellPermissionIdentity();
|
||||
sTNM = (TestNetworkManager) sContext.getSystemService(Context.TEST_NETWORK_SERVICE);
|
||||
|
||||
// Under normal circumstances, the MANAGE_IPSEC_TUNNELS appop would be auto-granted, and
|
||||
// a standard permission is insufficient. So we shell out the appop, to give us the
|
||||
// right appop permissions.
|
||||
setAppOp(OP_MANAGE_IPSEC_TUNNELS, true);
|
||||
}
|
||||
|
||||
// This method is guaranteed to run in subclasses and will run after subclasses' @AfterClass
|
||||
// methods.
|
||||
@AfterClass
|
||||
public static void tearDownPermissionAfterClass() throws Exception {
|
||||
setAppOp(OP_MANAGE_IPSEC_TUNNELS, false);
|
||||
|
||||
InstrumentationRegistry.getInstrumentation()
|
||||
.getUiAutomation()
|
||||
.dropShellPermissionIdentity();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
mLocalAddress = getNextAvailableIpv4AddressLocal();
|
||||
mRemoteAddress = getNextAvailableIpv4AddressRemote();
|
||||
setUpTestNetwork(mLocalAddress);
|
||||
|
||||
mUserCbExecutor = Executors.newSingleThreadExecutor();
|
||||
mIkeSessionCallback = new TestIkeSessionCallback();
|
||||
mFirstChildSessionCallback = new TestChildSessionCallback();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
tearDownTestNetwork();
|
||||
|
||||
resetNextAvailableAddress(NEXT_AVAILABLE_IP4_ADDR_LOCAL, INITIAL_AVAILABLE_IP4_ADDR_LOCAL);
|
||||
resetNextAvailableAddress(
|
||||
NEXT_AVAILABLE_IP4_ADDR_REMOTE, INITIAL_AVAILABLE_IP4_ADDR_REMOTE);
|
||||
}
|
||||
|
||||
void setUpTestNetwork(InetAddress localAddr) throws Exception {
|
||||
int prefixLen = localAddr instanceof Inet4Address ? IP4_PREFIX_LEN : IP4_PREFIX_LEN;
|
||||
|
||||
TestNetworkInterface testIface =
|
||||
sTNM.createTunInterface(new LinkAddress[] {new LinkAddress(localAddr, prefixLen)});
|
||||
|
||||
mTunFd = testIface.getFileDescriptor();
|
||||
mTunNetworkCallback =
|
||||
TestNetworkUtils.setupAndGetTestNetwork(
|
||||
sCM, sTNM, testIface.getInterfaceName(), new Binder());
|
||||
mTunNetwork = mTunNetworkCallback.getNetworkBlocking();
|
||||
mTunUtils = new IkeTunUtils(mTunFd);
|
||||
}
|
||||
|
||||
void tearDownTestNetwork() throws Exception {
|
||||
sCM.unregisterNetworkCallback(mTunNetworkCallback);
|
||||
|
||||
sTNM.teardownTestNetwork(mTunNetwork);
|
||||
mTunFd.close();
|
||||
}
|
||||
|
||||
private static void setAppOp(int appop, boolean allow) {
|
||||
String opName = AppOpsManager.opToName(appop);
|
||||
for (String pkg : new String[] {"com.android.shell", sContext.getPackageName()}) {
|
||||
String cmd =
|
||||
String.format(
|
||||
"appops set %s %s %s",
|
||||
pkg, // Package name
|
||||
opName, // Appop
|
||||
(allow ? "allow" : "deny")); // Action
|
||||
Log.d("IKE", "CTS setAppOp cmd " + cmd);
|
||||
|
||||
String result = SystemUtil.runShellCommand(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
Inet4Address getNextAvailableIpv4AddressLocal() throws Exception {
|
||||
return (Inet4Address)
|
||||
getNextAvailableAddress(
|
||||
NEXT_AVAILABLE_IP4_ADDR_LOCAL,
|
||||
INITIAL_AVAILABLE_IP4_ADDR_LOCAL,
|
||||
false /* isIp6 */);
|
||||
}
|
||||
|
||||
Inet4Address getNextAvailableIpv4AddressRemote() throws Exception {
|
||||
return (Inet4Address)
|
||||
getNextAvailableAddress(
|
||||
NEXT_AVAILABLE_IP4_ADDR_REMOTE,
|
||||
INITIAL_AVAILABLE_IP4_ADDR_REMOTE,
|
||||
false /* isIp6 */);
|
||||
}
|
||||
|
||||
InetAddress getNextAvailableAddress(
|
||||
byte[] nextAddressBytes, byte[] initialAddressBytes, boolean isIp6) throws Exception {
|
||||
int addressLen = isIp6 ? IP6_ADDRESS_LEN : IP4_ADDRESS_LEN;
|
||||
|
||||
synchronized (nextAddressBytes) {
|
||||
if (nextAddressBytes[addressLen - 1] == IP_ADDR_LAST_BYTE_MAX) {
|
||||
resetNextAvailableAddress(nextAddressBytes, initialAddressBytes);
|
||||
}
|
||||
|
||||
InetAddress address = InetAddress.getByAddress(nextAddressBytes);
|
||||
nextAddressBytes[addressLen - 1]++;
|
||||
return address;
|
||||
}
|
||||
}
|
||||
|
||||
private void resetNextAvailableAddress(byte[] nextAddressBytes, byte[] initialAddressBytes) {
|
||||
synchronized (nextAddressBytes) {
|
||||
System.arraycopy(
|
||||
nextAddressBytes, 0, initialAddressBytes, 0, initialAddressBytes.length);
|
||||
}
|
||||
}
|
||||
|
||||
static class TestIkeSessionCallback implements IkeSessionCallback {
|
||||
private CompletableFuture<IkeSessionConfiguration> mFutureIkeConfig =
|
||||
new CompletableFuture<>();
|
||||
private CompletableFuture<Boolean> mFutureOnClosedCall = new CompletableFuture<>();
|
||||
private CompletableFuture<IkeException> mFutureOnClosedException =
|
||||
new CompletableFuture<>();
|
||||
|
||||
private int mOnErrorExceptionsCount = 0;
|
||||
private ArrayTrackRecord<IkeProtocolException> mOnErrorExceptionsTrackRecord =
|
||||
new ArrayTrackRecord<>();
|
||||
|
||||
@Override
|
||||
public void onOpened(@NonNull IkeSessionConfiguration sessionConfiguration) {
|
||||
mFutureIkeConfig.complete(sessionConfiguration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClosed() {
|
||||
mFutureOnClosedCall.complete(true /* unused */);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClosedExceptionally(@NonNull IkeException exception) {
|
||||
mFutureOnClosedException.complete(exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@NonNull IkeProtocolException exception) {
|
||||
mOnErrorExceptionsTrackRecord.add(exception);
|
||||
}
|
||||
|
||||
public IkeSessionConfiguration awaitIkeConfig() throws Exception {
|
||||
return mFutureIkeConfig.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public IkeException awaitOnClosedException() throws Exception {
|
||||
return mFutureOnClosedException.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public IkeProtocolException awaitNextOnErrorException() {
|
||||
return mOnErrorExceptionsTrackRecord.poll(
|
||||
(long) TIMEOUT_MS,
|
||||
mOnErrorExceptionsCount++,
|
||||
(transform) -> {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public void awaitOnClosed() throws Exception {
|
||||
mFutureOnClosedCall.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
static class TestChildSessionCallback implements ChildSessionCallback {
|
||||
private CompletableFuture<ChildSessionConfiguration> mFutureChildConfig =
|
||||
new CompletableFuture<>();
|
||||
private CompletableFuture<Boolean> mFutureOnClosedCall = new CompletableFuture<>();
|
||||
private CompletableFuture<IkeException> mFutureOnClosedException =
|
||||
new CompletableFuture<>();
|
||||
|
||||
private int mCreatedIpSecTransformCount = 0;
|
||||
private int mDeletedIpSecTransformCount = 0;
|
||||
private ArrayTrackRecord<IpSecTransformCallRecord> mCreatedIpSecTransformsTrackRecord =
|
||||
new ArrayTrackRecord<>();
|
||||
private ArrayTrackRecord<IpSecTransformCallRecord> mDeletedIpSecTransformsTrackRecord =
|
||||
new ArrayTrackRecord<>();
|
||||
|
||||
@Override
|
||||
public void onOpened(@NonNull ChildSessionConfiguration sessionConfiguration) {
|
||||
mFutureChildConfig.complete(sessionConfiguration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClosed() {
|
||||
mFutureOnClosedCall.complete(true /* unused */);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClosedExceptionally(@NonNull IkeException exception) {
|
||||
mFutureOnClosedException.complete(exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIpSecTransformCreated(@NonNull IpSecTransform ipSecTransform, int direction) {
|
||||
mCreatedIpSecTransformsTrackRecord.add(
|
||||
new IpSecTransformCallRecord(ipSecTransform, direction));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIpSecTransformDeleted(@NonNull IpSecTransform ipSecTransform, int direction) {
|
||||
mDeletedIpSecTransformsTrackRecord.add(
|
||||
new IpSecTransformCallRecord(ipSecTransform, direction));
|
||||
}
|
||||
|
||||
public ChildSessionConfiguration awaitChildConfig() throws Exception {
|
||||
return mFutureChildConfig.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public IkeException awaitOnClosedException() throws Exception {
|
||||
return mFutureOnClosedException.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public IpSecTransformCallRecord awaitNextCreatedIpSecTransform() {
|
||||
return mCreatedIpSecTransformsTrackRecord.poll(
|
||||
(long) TIMEOUT_MS,
|
||||
mCreatedIpSecTransformCount++,
|
||||
(transform) -> {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public IpSecTransformCallRecord awaitNextDeletedIpSecTransform() {
|
||||
return mDeletedIpSecTransformsTrackRecord.poll(
|
||||
(long) TIMEOUT_MS,
|
||||
mDeletedIpSecTransformCount++,
|
||||
(transform) -> {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public void awaitOnClosed() throws Exception {
|
||||
mFutureOnClosedCall.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class represents a created or deleted IpSecTransfrom that is provided by
|
||||
* ChildSessionCallback
|
||||
*/
|
||||
static class IpSecTransformCallRecord {
|
||||
public final IpSecTransform ipSecTransform;
|
||||
public final int direction;
|
||||
|
||||
IpSecTransformCallRecord(IpSecTransform ipSecTransform, @PolicyDirection int direction) {
|
||||
this.ipSecTransform = ipSecTransform;
|
||||
this.direction = direction;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(b/148689509): Verify IKE Session setup using EAP and digital-signature-based auth
|
||||
|
||||
// TODO(b/148689509): Verify hostname based creation
|
||||
}
|
||||
@@ -31,13 +31,15 @@ import java.util.Map;
|
||||
|
||||
/** Shared parameters and util methods for testing different components of IKE */
|
||||
abstract class IkeTestBase {
|
||||
private static final int MIN_PORT = 0;
|
||||
private static final int MAX_PORT = 65535;
|
||||
static final int MIN_PORT = 0;
|
||||
static final int MAX_PORT = 65535;
|
||||
private static final int INBOUND_TS_START_PORT = MIN_PORT;
|
||||
private static final int INBOUND_TS_END_PORT = 65520;
|
||||
private static final int OUTBOUND_TS_START_PORT = 16;
|
||||
private static final int OUTBOUND_TS_END_PORT = MAX_PORT;
|
||||
|
||||
static final int IP4_ADDRESS_LEN = 4;
|
||||
static final int IP6_ADDRESS_LEN = 16;
|
||||
static final int IP4_PREFIX_LEN = 32;
|
||||
static final int IP6_PREFIX_LEN = 64;
|
||||
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package android.net.ipsec.ike.cts;
|
||||
|
||||
import static android.net.ipsec.ike.cts.PacketUtils.BytePayload;
|
||||
import static android.net.ipsec.ike.cts.PacketUtils.IP4_HDRLEN;
|
||||
import static android.net.ipsec.ike.cts.PacketUtils.IP6_HDRLEN;
|
||||
import static android.net.ipsec.ike.cts.PacketUtils.Ip4Header;
|
||||
import static android.net.ipsec.ike.cts.PacketUtils.Ip6Header;
|
||||
import static android.net.ipsec.ike.cts.PacketUtils.IpHeader;
|
||||
import static android.net.ipsec.ike.cts.PacketUtils.Payload;
|
||||
import static android.net.ipsec.ike.cts.PacketUtils.UDP_HDRLEN;
|
||||
import static android.net.ipsec.ike.cts.PacketUtils.UdpHeader;
|
||||
import static android.system.OsConstants.IPPROTO_UDP;
|
||||
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class IkeTunUtils extends TunUtils {
|
||||
private static final int PORT_LEN = 2;
|
||||
|
||||
private static final int NON_ESP_MARKER_LEN = 4;
|
||||
private static final byte[] NON_ESP_MARKER = new byte[NON_ESP_MARKER_LEN];
|
||||
|
||||
private static final int IKE_HEADER_LEN = 28;
|
||||
private static final int IKE_INIT_SPI_OFFSET = 0;
|
||||
private static final int IKE_IS_RESP_BYTE_OFFSET = 19;
|
||||
private static final int IKE_MSG_ID_OFFSET = 20;
|
||||
|
||||
public IkeTunUtils(ParcelFileDescriptor tunFd) {
|
||||
super(tunFd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Await the expected IKE request and inject an IKE response.
|
||||
*
|
||||
* @param respIkePkt IKE response packet without IP/UDP headers or NON ESP MARKER.
|
||||
*/
|
||||
public byte[] awaitReqAndInjectResp(
|
||||
long expectedInitIkeSpi, int expectedMsgId, boolean expectedUseEncap, byte[] respIkePkt)
|
||||
throws Exception {
|
||||
byte[] request =
|
||||
awaitIkePacket(
|
||||
expectedInitIkeSpi,
|
||||
expectedMsgId,
|
||||
false /* expectedResp */,
|
||||
expectedUseEncap);
|
||||
|
||||
// Build response header by flipping address and port
|
||||
InetAddress srcAddr = getAddress(request, false /* shouldGetSource */);
|
||||
InetAddress dstAddr = getAddress(request, true /* shouldGetSource */);
|
||||
int srcPort = getPort(request, false /* shouldGetSource */);
|
||||
int dstPort = getPort(request, true /* shouldGetSource */);
|
||||
|
||||
byte[] response =
|
||||
buildIkePacket(srcAddr, dstAddr, srcPort, dstPort, expectedUseEncap, respIkePkt);
|
||||
injectPacket(response);
|
||||
return request;
|
||||
}
|
||||
|
||||
private byte[] awaitIkePacket(
|
||||
long expectedInitIkeSpi,
|
||||
int expectedMsgId,
|
||||
boolean expectedResp,
|
||||
boolean expectedUseEncap)
|
||||
throws Exception {
|
||||
long endTime = System.currentTimeMillis() + TIMEOUT;
|
||||
int startIndex = 0;
|
||||
synchronized (mPackets) {
|
||||
while (System.currentTimeMillis() < endTime) {
|
||||
byte[] ikePkt =
|
||||
getFirstMatchingPacket(
|
||||
(pkt) -> {
|
||||
return isIke(
|
||||
pkt,
|
||||
expectedInitIkeSpi,
|
||||
expectedMsgId,
|
||||
expectedResp,
|
||||
expectedUseEncap);
|
||||
},
|
||||
startIndex);
|
||||
if (ikePkt != null) {
|
||||
return ikePkt; // We've found the packet we're looking for.
|
||||
}
|
||||
|
||||
startIndex = mPackets.size();
|
||||
|
||||
// Try to prevent waiting too long. If waitTimeout <= 0, we've already hit timeout
|
||||
long waitTimeout = endTime - System.currentTimeMillis();
|
||||
if (waitTimeout > 0) {
|
||||
mPackets.wait(waitTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
String direction = expectedResp ? "response" : "request";
|
||||
fail(
|
||||
"No such IKE "
|
||||
+ direction
|
||||
+ " found with Initiator SPI "
|
||||
+ expectedInitIkeSpi
|
||||
+ " and message ID "
|
||||
+ expectedMsgId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isIke(
|
||||
byte[] pkt,
|
||||
long expectedInitIkeSpi,
|
||||
int expectedMsgId,
|
||||
boolean expectedResp,
|
||||
boolean expectedUseEncap) {
|
||||
int ipProtocolOffset = 0;
|
||||
int ikeOffset = 0;
|
||||
if (isIpv6(pkt)) {
|
||||
// IPv6 UDP expectedUseEncap not supported by kernels; assume non-expectedUseEncap.
|
||||
ipProtocolOffset = IP6_PROTO_OFFSET;
|
||||
ikeOffset = IP6_HDRLEN + UDP_HDRLEN;
|
||||
} else {
|
||||
// Use default IPv4 header length (assuming no options)
|
||||
ipProtocolOffset = IP4_PROTO_OFFSET;
|
||||
ikeOffset = IP4_HDRLEN + UDP_HDRLEN;
|
||||
|
||||
if (expectedUseEncap) {
|
||||
if (hasNonEspMarker(pkt)) {
|
||||
ikeOffset += NON_ESP_MARKER_LEN;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pkt[ipProtocolOffset] == IPPROTO_UDP
|
||||
&& areSpiAndMsgIdEqual(
|
||||
pkt, ikeOffset, expectedInitIkeSpi, expectedMsgId, expectedResp);
|
||||
}
|
||||
|
||||
private static boolean hasNonEspMarker(byte[] pkt) {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(pkt);
|
||||
int ikeOffset = IP4_HDRLEN + UDP_HDRLEN;
|
||||
if (buffer.remaining() < ikeOffset) return false;
|
||||
|
||||
buffer.get(new byte[ikeOffset]); // Skip IP and UDP header
|
||||
byte[] nonEspMarker = new byte[NON_ESP_MARKER_LEN];
|
||||
if (buffer.remaining() < NON_ESP_MARKER_LEN) return false;
|
||||
|
||||
buffer.get(nonEspMarker);
|
||||
return Arrays.equals(NON_ESP_MARKER, nonEspMarker);
|
||||
}
|
||||
|
||||
private static boolean areSpiAndMsgIdEqual(
|
||||
byte[] pkt,
|
||||
int ikeOffset,
|
||||
long expectedIkeInitSpi,
|
||||
int expectedMsgId,
|
||||
boolean expectedResp) {
|
||||
if (pkt.length <= ikeOffset + IKE_HEADER_LEN) return false;
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.wrap(pkt);
|
||||
buffer.get(new byte[ikeOffset]); // Skip IP, UDP header (and NON_ESP_MARKER)
|
||||
|
||||
// Check message ID.
|
||||
buffer.get(new byte[IKE_MSG_ID_OFFSET]);
|
||||
int msgId = buffer.getInt();
|
||||
return expectedMsgId == msgId;
|
||||
|
||||
// TODO: Check SPI and packet direction
|
||||
}
|
||||
|
||||
private static InetAddress getAddress(byte[] pkt, boolean shouldGetSource) throws Exception {
|
||||
int ipLen = isIpv6(pkt) ? IP6_ADDR_LEN : IP4_ADDR_LEN;
|
||||
int srcIpOffset = isIpv6(pkt) ? IP6_ADDR_OFFSET : IP4_ADDR_OFFSET;
|
||||
int ipOffset = shouldGetSource ? srcIpOffset : srcIpOffset + ipLen;
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.wrap(pkt);
|
||||
buffer.get(new byte[ipOffset]);
|
||||
byte[] ipAddrBytes = new byte[ipLen];
|
||||
buffer.get(ipAddrBytes);
|
||||
return InetAddress.getByAddress(ipAddrBytes);
|
||||
}
|
||||
|
||||
private static int getPort(byte[] pkt, boolean shouldGetSource) {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(pkt);
|
||||
int srcPortOffset = isIpv6(pkt) ? IP6_HDRLEN : IP4_HDRLEN;
|
||||
int portOffset = shouldGetSource ? srcPortOffset : srcPortOffset + PORT_LEN;
|
||||
|
||||
buffer.get(new byte[portOffset]);
|
||||
return Short.toUnsignedInt(buffer.getShort());
|
||||
}
|
||||
|
||||
private static byte[] buildIkePacket(
|
||||
InetAddress srcAddr,
|
||||
InetAddress dstAddr,
|
||||
int srcPort,
|
||||
int dstPort,
|
||||
boolean useEncap,
|
||||
byte[] ikePacket)
|
||||
throws Exception {
|
||||
if (useEncap) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(NON_ESP_MARKER_LEN + ikePacket.length);
|
||||
buffer.put(NON_ESP_MARKER);
|
||||
buffer.put(ikePacket);
|
||||
ikePacket = buffer.array();
|
||||
}
|
||||
|
||||
UdpHeader udpPkt = new UdpHeader(srcPort, dstPort, new BytePayload(ikePacket));
|
||||
IpHeader ipPkt = getIpHeader(udpPkt.getProtocolId(), srcAddr, dstAddr, udpPkt);
|
||||
return ipPkt.getPacketBytes();
|
||||
}
|
||||
|
||||
private static IpHeader getIpHeader(
|
||||
int protocol, InetAddress src, InetAddress dst, Payload payload) {
|
||||
if ((src instanceof Inet6Address) != (dst instanceof Inet6Address)) {
|
||||
throw new IllegalArgumentException("Invalid src/dst address combination");
|
||||
}
|
||||
|
||||
if (src instanceof Inet6Address) {
|
||||
return new Ip6Header(protocol, (Inet6Address) src, (Inet6Address) dst, payload);
|
||||
} else {
|
||||
return new Ip4Header(protocol, (Inet4Address) src, (Inet4Address) dst, payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,18 +47,18 @@ public class TunUtils {
|
||||
private static final String TAG = TunUtils.class.getSimpleName();
|
||||
|
||||
private static final int DATA_BUFFER_LEN = 4096;
|
||||
private static final int TIMEOUT = 100;
|
||||
static final int TIMEOUT = 100;
|
||||
|
||||
private static final int IP4_PROTO_OFFSET = 9;
|
||||
private static final int IP6_PROTO_OFFSET = 6;
|
||||
static final int IP4_PROTO_OFFSET = 9;
|
||||
static final int IP6_PROTO_OFFSET = 6;
|
||||
|
||||
private static final int IP4_ADDR_OFFSET = 12;
|
||||
private static final int IP4_ADDR_LEN = 4;
|
||||
private static final int IP6_ADDR_OFFSET = 8;
|
||||
private static final int IP6_ADDR_LEN = 16;
|
||||
static final int IP4_ADDR_OFFSET = 12;
|
||||
static final int IP4_ADDR_LEN = 4;
|
||||
static final int IP6_ADDR_OFFSET = 8;
|
||||
static final int IP6_ADDR_LEN = 16;
|
||||
|
||||
final List<byte[]> mPackets = new ArrayList<>();
|
||||
private final ParcelFileDescriptor mTunFd;
|
||||
private final List<byte[]> mPackets = new ArrayList<>();
|
||||
private final Thread mReaderThread;
|
||||
|
||||
public TunUtils(ParcelFileDescriptor tunFd) {
|
||||
@@ -107,7 +107,7 @@ public class TunUtils {
|
||||
return Arrays.copyOf(inBytes, bytesRead);
|
||||
}
|
||||
|
||||
private byte[] getFirstMatchingPacket(Predicate<byte[]> verifier, int startIndex) {
|
||||
byte[] getFirstMatchingPacket(Predicate<byte[]> verifier, int startIndex) {
|
||||
synchronized (mPackets) {
|
||||
for (int i = startIndex; i < mPackets.size(); i++) {
|
||||
byte[] pkt = mPackets.get(i);
|
||||
@@ -198,7 +198,7 @@ public class TunUtils {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isIpv6(byte[] pkt) {
|
||||
static boolean isIpv6(byte[] pkt) {
|
||||
// First nibble shows IP version. 0x60 for IPv6
|
||||
return (pkt[0] & (byte) 0xF0) == (byte) 0x60;
|
||||
}
|
||||
|
||||
254
tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
Normal file
254
tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
Normal file
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.net.cts
|
||||
|
||||
import android.Manifest.permission.NETWORK_SETTINGS
|
||||
import android.Manifest.permission.READ_DEVICE_CONFIG
|
||||
import android.Manifest.permission.WRITE_DEVICE_CONFIG
|
||||
import android.content.pm.PackageManager.FEATURE_TELEPHONY
|
||||
import android.content.pm.PackageManager.FEATURE_WIFI
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.ConnectivityManager.NetworkCallback
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
|
||||
import android.net.NetworkCapabilities.TRANSPORT_WIFI
|
||||
import android.net.NetworkRequest
|
||||
import android.net.Uri
|
||||
import android.net.cts.util.CtsNetUtils
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.ConditionVariable
|
||||
import android.platform.test.annotations.AppModeFull
|
||||
import android.provider.DeviceConfig
|
||||
import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
|
||||
import android.text.TextUtils
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import com.android.compatibility.common.util.SystemUtil
|
||||
import fi.iki.elonen.NanoHTTPD
|
||||
import fi.iki.elonen.NanoHTTPD.Response.IStatus
|
||||
import fi.iki.elonen.NanoHTTPD.Response.Status
|
||||
import junit.framework.AssertionFailedError
|
||||
import org.junit.After
|
||||
import org.junit.Assume.assumeTrue
|
||||
import org.junit.Before
|
||||
import org.junit.runner.RunWith
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.TimeoutException
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertNotEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
private const val TEST_CAPTIVE_PORTAL_HTTPS_URL_SETTING = "test_captive_portal_https_url"
|
||||
private const val TEST_CAPTIVE_PORTAL_HTTP_URL_SETTING = "test_captive_portal_http_url"
|
||||
private const val TEST_URL_EXPIRATION_TIME = "test_url_expiration_time"
|
||||
|
||||
private const val TEST_HTTPS_URL_PATH = "https_path"
|
||||
private const val TEST_HTTP_URL_PATH = "http_path"
|
||||
private const val TEST_PORTAL_URL_PATH = "portal_path"
|
||||
|
||||
private const val LOCALHOST_HOSTNAME = "localhost"
|
||||
|
||||
// Re-connecting to the AP, obtaining an IP address, revalidating can take a long time
|
||||
private const val WIFI_CONNECT_TIMEOUT_MS = 120_000L
|
||||
private const val TEST_TIMEOUT_MS = 10_000L
|
||||
|
||||
private fun <T> CompletableFuture<T>.assertGet(timeoutMs: Long, message: String): T {
|
||||
try {
|
||||
return get(timeoutMs, TimeUnit.MILLISECONDS)
|
||||
} catch (e: TimeoutException) {
|
||||
throw AssertionFailedError(message)
|
||||
}
|
||||
}
|
||||
|
||||
@AppModeFull(reason = "WRITE_DEVICE_CONFIG permission can't be granted to instant apps")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class CaptivePortalTest {
|
||||
private val context: android.content.Context by lazy { getInstrumentation().context }
|
||||
private val wm by lazy { context.getSystemService(WifiManager::class.java) }
|
||||
private val cm by lazy { context.getSystemService(ConnectivityManager::class.java) }
|
||||
private val pm by lazy { context.packageManager }
|
||||
private val utils by lazy { CtsNetUtils(context) }
|
||||
|
||||
private val server = HttpServer()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
doAsShell(READ_DEVICE_CONFIG) {
|
||||
// Verify that the test URLs are not normally set on the device, but do not fail if the
|
||||
// test URLs are set to what this test uses (URLs on localhost), in case the test was
|
||||
// interrupted manually and rerun.
|
||||
assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL_SETTING)
|
||||
assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTP_URL_SETTING)
|
||||
}
|
||||
clearTestUrls()
|
||||
server.start()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
clearTestUrls()
|
||||
server.stop()
|
||||
}
|
||||
|
||||
private fun assertEmptyOrLocalhostUrl(urlKey: String) {
|
||||
val url = DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, urlKey)
|
||||
assertTrue(TextUtils.isEmpty(url) || LOCALHOST_HOSTNAME == Uri.parse(url).host,
|
||||
"$urlKey must not be set in production scenarios (current value: $url)")
|
||||
}
|
||||
|
||||
private fun clearTestUrls() {
|
||||
setHttpsUrl(null)
|
||||
setHttpUrl(null)
|
||||
setUrlExpiration(null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCaptivePortalIsNotDefaultNetwork() {
|
||||
assumeTrue(pm.hasSystemFeature(FEATURE_TELEPHONY))
|
||||
assumeTrue(pm.hasSystemFeature(FEATURE_WIFI))
|
||||
utils.connectToWifi()
|
||||
utils.connectToCell()
|
||||
|
||||
// Have network validation use a local server that serves a HTTPS error / HTTP redirect
|
||||
server.addResponse(TEST_PORTAL_URL_PATH, Status.OK,
|
||||
content = "Test captive portal content")
|
||||
server.addResponse(TEST_HTTPS_URL_PATH, Status.INTERNAL_ERROR)
|
||||
server.addResponse(TEST_HTTP_URL_PATH, Status.REDIRECT,
|
||||
locationHeader = server.makeUrl(TEST_PORTAL_URL_PATH))
|
||||
setHttpsUrl(server.makeUrl(TEST_HTTPS_URL_PATH))
|
||||
setHttpUrl(server.makeUrl(TEST_HTTP_URL_PATH))
|
||||
// URL expiration needs to be in the next 10 minutes
|
||||
setUrlExpiration(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(9))
|
||||
|
||||
// Expect the portal content to be fetched at some point after detecting the portal.
|
||||
// Some implementations may fetch the URL before startCaptivePortalApp is called.
|
||||
val portalContentRequestCv = server.addExpectRequestCv(TEST_PORTAL_URL_PATH)
|
||||
|
||||
// Wait for a captive portal to be detected on the network
|
||||
val wifiNetworkFuture = CompletableFuture<Network>()
|
||||
val wifiCb = object : NetworkCallback() {
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
nc: NetworkCapabilities
|
||||
) {
|
||||
if (nc.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) {
|
||||
wifiNetworkFuture.complete(network)
|
||||
}
|
||||
}
|
||||
}
|
||||
cm.requestNetwork(NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build(), wifiCb)
|
||||
|
||||
try {
|
||||
reconnectWifi()
|
||||
val network = wifiNetworkFuture.assertGet(WIFI_CONNECT_TIMEOUT_MS,
|
||||
"Captive portal not detected after ${WIFI_CONNECT_TIMEOUT_MS}ms")
|
||||
|
||||
val wifiDefaultMessage = "Wifi should not be the default network when a captive " +
|
||||
"portal was detected and another network (mobile data) can provide internet " +
|
||||
"access."
|
||||
assertNotEquals(network, cm.activeNetwork, wifiDefaultMessage)
|
||||
|
||||
doAsShell(NETWORK_SETTINGS) { cm.startCaptivePortalApp(network) }
|
||||
assertTrue(portalContentRequestCv.block(TEST_TIMEOUT_MS), "The captive portal login " +
|
||||
"page was still not fetched ${TEST_TIMEOUT_MS}ms after startCaptivePortalApp.")
|
||||
|
||||
assertNotEquals(network, cm.activeNetwork, wifiDefaultMessage)
|
||||
} finally {
|
||||
cm.unregisterNetworkCallback(wifiCb)
|
||||
server.stop()
|
||||
// disconnectFromCell should be called after connectToCell
|
||||
utils.disconnectFromCell()
|
||||
}
|
||||
|
||||
clearTestUrls()
|
||||
reconnectWifi()
|
||||
}
|
||||
|
||||
private fun setHttpsUrl(url: String?) = setConfig(TEST_CAPTIVE_PORTAL_HTTPS_URL_SETTING, url)
|
||||
private fun setHttpUrl(url: String?) = setConfig(TEST_CAPTIVE_PORTAL_HTTP_URL_SETTING, url)
|
||||
private fun setUrlExpiration(timestamp: Long?) = setConfig(TEST_URL_EXPIRATION_TIME,
|
||||
timestamp?.toString())
|
||||
|
||||
private fun setConfig(configKey: String, value: String?) {
|
||||
doAsShell(WRITE_DEVICE_CONFIG) {
|
||||
DeviceConfig.setProperty(
|
||||
NAMESPACE_CONNECTIVITY, configKey, value, false /* makeDefault */)
|
||||
}
|
||||
}
|
||||
|
||||
private fun doAsShell(vararg permissions: String, action: () -> Unit) {
|
||||
// Wrap the below call to allow for more kotlin-like syntax
|
||||
SystemUtil.runWithShellPermissionIdentity(action, permissions)
|
||||
}
|
||||
|
||||
private fun reconnectWifi() {
|
||||
doAsShell(NETWORK_SETTINGS) {
|
||||
assertTrue(wm.disconnect())
|
||||
assertTrue(wm.reconnect())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A minimal HTTP server running on localhost (loopback), on a random available port.
|
||||
*/
|
||||
private class HttpServer : NanoHTTPD("localhost", 0 /* auto-select the port */) {
|
||||
// Map of URL path -> HTTP response code
|
||||
private val responses = HashMap<String, Response>()
|
||||
|
||||
// Map of path -> CV to open as soon as a request to the path is received
|
||||
private val waitForRequestCv = HashMap<String, ConditionVariable>()
|
||||
|
||||
/**
|
||||
* Create a URL string that, when fetched, will hit this server with the given URL [path].
|
||||
*/
|
||||
fun makeUrl(path: String): String {
|
||||
return Uri.Builder()
|
||||
.scheme("http")
|
||||
.encodedAuthority("localhost:$listeningPort")
|
||||
.query(path)
|
||||
.build()
|
||||
.toString()
|
||||
}
|
||||
|
||||
fun addResponse(
|
||||
path: String,
|
||||
statusCode: IStatus,
|
||||
locationHeader: String? = null,
|
||||
content: String = ""
|
||||
) {
|
||||
val response = newFixedLengthResponse(statusCode, "text/plain", content)
|
||||
locationHeader?.let { response.addHeader("Location", it) }
|
||||
responses[path] = response
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a [ConditionVariable] that will open when a request to [path] is received.
|
||||
*/
|
||||
fun addExpectRequestCv(path: String): ConditionVariable {
|
||||
return ConditionVariable().apply { waitForRequestCv[path] = this }
|
||||
}
|
||||
|
||||
override fun serve(session: IHTTPSession): Response {
|
||||
waitForRequestCv[session.queryParameterString]?.open()
|
||||
return responses[session.queryParameterString]
|
||||
// Default response is a 404
|
||||
?: super.serve(session)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user