diff --git a/tests/src/test/java/org/eclipse/hono/tests/EnabledIfDnsRebindingIsSupported.java b/tests/src/test/java/org/eclipse/hono/tests/EnabledIfDnsRebindingIsSupported.java new file mode 100644 index 0000000000..5540ccdae4 --- /dev/null +++ b/tests/src/test/java/org/eclipse/hono/tests/EnabledIfDnsRebindingIsSupported.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.hono.tests; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * An annotation which configures a test to run only if DNS rebinding for the nip.io domain works in the + * local environment. + */ +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(EnabledIfDnsRebindingIsSupportedCondition.class) +public @interface EnabledIfDnsRebindingIsSupported { + + /** + * The default domain name. + */ + String DEFAULT_DOMAIN = "nip.io"; + + /** + * The domain to use for checking DNS rebinding to work. + *

+ * The default value of this property is {@value #DEFAULT_DOMAIN}. + * + * @return The domain name. + */ + String domain() default DEFAULT_DOMAIN; +} diff --git a/tests/src/test/java/org/eclipse/hono/tests/EnabledIfDnsRebindingIsSupportedCondition.java b/tests/src/test/java/org/eclipse/hono/tests/EnabledIfDnsRebindingIsSupportedCondition.java new file mode 100644 index 0000000000..8887b267d6 --- /dev/null +++ b/tests/src/test/java/org/eclipse/hono/tests/EnabledIfDnsRebindingIsSupportedCondition.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + + +package org.eclipse.hono.tests; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.platform.commons.util.AnnotationUtils; + + +/** + * A condition that checks if DNS rebinding works for host names containing a particular domain. + *

+ * This makes sure that a look up of a host name like {@code 127.0.0.1.nip.io} is successfully resolved to IPv4 + * address {@code 127.0.0.1}. + *

+ * The domain name to check is taken from {@link EnabledIfDnsRebindingIsSupported#domain()}. + */ +public class EnabledIfDnsRebindingIsSupportedCondition implements ExecutionCondition { + + private static final Map RESULTS = new HashMap<>(); + + /** + * Checks if {@code 127.0.0.1.nip.io} can be resolved to {@code 127.0.0.1}. + * + * @param context The context to evaluate in. + */ + @Override + public ConditionEvaluationResult evaluateExecutionCondition(final ExtensionContext context) { + + final String domainName = AnnotationUtils.findAnnotation( + context.getElement(), + EnabledIfDnsRebindingIsSupported.class) + .map(EnabledIfDnsRebindingIsSupported::domain) + .orElse(EnabledIfDnsRebindingIsSupported.DEFAULT_DOMAIN); + + synchronized (RESULTS) { + return RESULTS.computeIfAbsent(domainName, this::performLookup); + } + } + + private ConditionEvaluationResult performLookup(final String domainName) { + + + final String hostname = "127.0.0.1.%s".formatted(domainName); + try { + final var address = InetAddress.getByName(hostname); + if (address.isLoopbackAddress()) { + return ConditionEvaluationResult.enabled("lookup of %s succeeded".formatted(hostname)); + } else { + return ConditionEvaluationResult.disabled("lookup of %s yields non-loopback address: %s" + .formatted(hostname, address.getHostAddress())); + } + } catch (final UnknownHostException e) { + // DNS rebinding protection seems to be in place + return ConditionEvaluationResult.disabled(""" + DNS rebinding protection prevents resolving of %s. You might want to configure your resolver + to use a DNS server that allows rebinding for domain %s as described in + https://github.com/IBM-Blockchain/blockchain-vscode-extension/issues/2878#issuecomment-890147917 + """.formatted(hostname, domainName)); + } + } +} diff --git a/tests/src/test/java/org/eclipse/hono/tests/IntegrationTestSupport.java b/tests/src/test/java/org/eclipse/hono/tests/IntegrationTestSupport.java index 9f1b57dc20..a229055bea 100644 --- a/tests/src/test/java/org/eclipse/hono/tests/IntegrationTestSupport.java +++ b/tests/src/test/java/org/eclipse/hono/tests/IntegrationTestSupport.java @@ -37,6 +37,7 @@ import java.util.OptionalInt; import java.util.Queue; import java.util.Set; +import java.util.StringJoiner; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -383,7 +384,7 @@ public final class IntegrationTestSupport { /** * The IP address of the CoAP protocol adapter. */ - public static final String COAP_HOST = IntegrationTestSupport.getResolvableHostname(PROPERTY_COAP_HOST); + public static final String COAP_HOST = System.getProperty(PROPERTY_COAP_HOST, DEFAULT_HOST); /** * The port number that the CoAP adapter listens on for requests. */ @@ -395,7 +396,7 @@ public final class IntegrationTestSupport { /** * The IP address of the HTTP protocol adapter. */ - public static final String HTTP_HOST = IntegrationTestSupport.getResolvableHostname(PROPERTY_HTTP_HOST); + public static final String HTTP_HOST = System.getProperty(PROPERTY_HTTP_HOST, DEFAULT_HOST); /** * The port number that the HTTP adapter listens on for requests. */ @@ -407,7 +408,7 @@ public final class IntegrationTestSupport { /** * The IP address of the MQTT protocol adapter. */ - public static final String MQTT_HOST = IntegrationTestSupport.getResolvableHostname(PROPERTY_MQTT_HOST); + public static final String MQTT_HOST = System.getProperty(PROPERTY_MQTT_HOST, DEFAULT_HOST); /** * The port number that the MQTT adapter listens on for connections. */ @@ -419,7 +420,7 @@ public final class IntegrationTestSupport { /** * The IP address of the AMQP protocol adapter. */ - public static final String AMQP_HOST = IntegrationTestSupport.getResolvableHostname(PROPERTY_AMQP_HOST); + public static final String AMQP_HOST = System.getProperty(PROPERTY_AMQP_HOST, DEFAULT_HOST); /** * The port number that the AMQP adapter listens on for connections. */ @@ -531,16 +532,20 @@ public IntegrationTestSupport(final Vertx vertx) { } /** - * Gets a host name/IP address that can be resolved via DNS from the value of a Java system property. + * Gets a host name in the {@code nip.io} domain for a given host name. * - * @param systemPropertyName The name of the property to read. + * @param hostname The host name. + * @param virtualHost The virtual host name to prepend. * @return The host name. */ - private static String getResolvableHostname(final String systemPropertyName) { - return Optional.of(System.getProperty(systemPropertyName, DEFAULT_HOST)) - .map(host -> "localhost".equals(host) ? DEFAULT_HOST : host) - .map(ipAddress -> ipAddress + ".nip.io") - .get(); + public static String getSniHostname(final String hostname, final String virtualHost) { + + Objects.requireNonNull(hostname); + final String literalIpAddress = "localhost".equals(hostname) ? DEFAULT_HOST : hostname; + final var b = new StringJoiner("."); + Optional.ofNullable(virtualHost).ifPresent(b::add); + b.add(literalIpAddress).add("nip.io"); + return b.toString(); } private static ClientConfigProperties getClientConfigProperties( diff --git a/tests/src/test/java/org/eclipse/hono/tests/amqp/AmqpConnectionIT.java b/tests/src/test/java/org/eclipse/hono/tests/amqp/AmqpConnectionIT.java index b4b7af3629..12c1ded8d6 100644 --- a/tests/src/test/java/org/eclipse/hono/tests/amqp/AmqpConnectionIT.java +++ b/tests/src/test/java/org/eclipse/hono/tests/amqp/AmqpConnectionIT.java @@ -28,6 +28,7 @@ import org.eclipse.hono.client.ClientErrorException; import org.eclipse.hono.service.management.tenant.Tenant; +import org.eclipse.hono.tests.EnabledIfDnsRebindingIsSupported; import org.eclipse.hono.tests.EnabledIfRegistrySupportsFeatures; import org.eclipse.hono.tests.IntegrationTestSupport; import org.eclipse.hono.tests.Tenants; @@ -153,6 +154,7 @@ public void testConnectX509SucceedsForRegisteredDevice(final String tlsVersion, */ @ParameterizedTest(name = IntegrationTestSupport.PARAMETERIZED_TEST_NAME_PATTERN) @ValueSource(strings = { IntegrationTestSupport.TLS_VERSION_1_2, IntegrationTestSupport.TLS_VERSION_1_3 }) + @EnabledIfDnsRebindingIsSupported @EnabledIfRegistrySupportsFeatures(trustAnchorGroups = true) public void testConnectX509SucceedsUsingSni(final String tlsVersion, final VertxTestContext ctx) { @@ -171,7 +173,7 @@ public void testConnectX509SucceedsUsingSni(final String tlsVersion, final Vertx deviceId, cert)) .compose(ok -> connectToAdapter( - tenantId + "." + IntegrationTestSupport.AMQP_HOST, + IntegrationTestSupport.getSniHostname(IntegrationTestSupport.AMQP_HOST, tenantId), deviceCert, tlsVersion)) .onComplete(ctx.succeeding(con -> { @@ -189,6 +191,7 @@ public void testConnectX509SucceedsUsingSni(final String tlsVersion, final Vertx */ @ParameterizedTest(name = IntegrationTestSupport.PARAMETERIZED_TEST_NAME_PATTERN) @ValueSource(strings = { IntegrationTestSupport.TLS_VERSION_1_2, IntegrationTestSupport.TLS_VERSION_1_3 }) + @EnabledIfDnsRebindingIsSupported @EnabledIfRegistrySupportsFeatures(trustAnchorGroups = true, tenantAlias = true) public void testConnectX509SucceedsUsingSniWithTenantAlias(final String tlsVersion, final VertxTestContext ctx) { @@ -209,7 +212,7 @@ public void testConnectX509SucceedsUsingSniWithTenantAlias(final String tlsVersi deviceId, cert)) .compose(ok -> connectToAdapter( - "test-alias." + IntegrationTestSupport.AMQP_HOST, + IntegrationTestSupport.getSniHostname(IntegrationTestSupport.AMQP_HOST, "test-alias"), deviceCert, tlsVersion)) .onComplete(ctx.succeeding(con -> { @@ -620,6 +623,7 @@ public void testConnectFailsForNonMatchingTrustAnchor(final VertxTestContext ctx * @param ctx The test context */ @Test + @EnabledIfDnsRebindingIsSupported @EnabledIfRegistrySupportsFeatures(trustAnchorGroups = true, tenantAlias = true) public void testConnectX509FailsUsingSniWithNonExistingTenantAlias(final VertxTestContext ctx) { @@ -640,7 +644,7 @@ public void testConnectX509FailsUsingSniWithNonExistingTenantAlias(final VertxTe deviceId, cert)) .compose(ok -> connectToAdapter( - "wrong-alias." + IntegrationTestSupport.AMQP_HOST, + IntegrationTestSupport.getSniHostname(IntegrationTestSupport.AMQP_HOST, "wrong-alias"), deviceCert, IntegrationTestSupport.TLS_VERSION_1_2)) .onComplete(ctx.failing(t -> { diff --git a/tests/src/test/java/org/eclipse/hono/tests/coap/TelemetryCoapIT.java b/tests/src/test/java/org/eclipse/hono/tests/coap/TelemetryCoapIT.java index a79951bcde..501a80a6dc 100644 --- a/tests/src/test/java/org/eclipse/hono/tests/coap/TelemetryCoapIT.java +++ b/tests/src/test/java/org/eclipse/hono/tests/coap/TelemetryCoapIT.java @@ -32,6 +32,7 @@ import org.eclipse.hono.application.client.MessageContext; import org.eclipse.hono.config.KeyLoader; import org.eclipse.hono.service.management.tenant.Tenant; +import org.eclipse.hono.tests.EnabledIfDnsRebindingIsSupported; import org.eclipse.hono.tests.EnabledIfRegistrySupportsFeatures; import org.eclipse.hono.tests.IntegrationTestSupport; import org.eclipse.hono.tests.Tenants; @@ -180,6 +181,7 @@ public void testRootResourceDoesNotExposeAnyInfo(final VertxTestContext ctx) thr * @throws InterruptedException if the test fails. */ @Test + @EnabledIfDnsRebindingIsSupported @EnabledIfRegistrySupportsFeatures(trustAnchorGroups = true, tenantAlias = true) public void testUploadMessagesUsingClientCertificateWithAlias(final VertxTestContext ctx) throws InterruptedException { @@ -212,11 +214,13 @@ public void testUploadMessagesUsingClientCertificateWithAlias(final VertxTestCon final CoapClient client = getCoapsClient(clientCertLoader); + final var hostname = IntegrationTestSupport.getSniHostname(IntegrationTestSupport.COAP_HOST, "test-alias"); + testUploadMessages(ctx, tenantId, () -> warmUp(client, createCoapsRequest( Code.POST, getMessageType(), - "test-alias." + IntegrationTestSupport.COAP_HOST, + hostname, getPostResource(), "hello 0".getBytes(StandardCharsets.UTF_8))), count -> { @@ -225,7 +229,7 @@ public void testUploadMessagesUsingClientCertificateWithAlias(final VertxTestCon final Request request = createCoapsRequest( Code.POST, getMessageType(), - "test-alias." + IntegrationTestSupport.COAP_HOST, + hostname, getPostResource(), payload.getBytes(StandardCharsets.UTF_8)); client.advanced(getHandler(result), request); diff --git a/tests/src/test/java/org/eclipse/hono/tests/http/TelemetryHttpIT.java b/tests/src/test/java/org/eclipse/hono/tests/http/TelemetryHttpIT.java index 9ca198a84d..6357b61225 100644 --- a/tests/src/test/java/org/eclipse/hono/tests/http/TelemetryHttpIT.java +++ b/tests/src/test/java/org/eclipse/hono/tests/http/TelemetryHttpIT.java @@ -29,6 +29,7 @@ import org.eclipse.hono.client.ServiceInvocationException; import org.eclipse.hono.service.management.tenant.Tenant; import org.eclipse.hono.tests.AssumeMessagingSystem; +import org.eclipse.hono.tests.EnabledIfDnsRebindingIsSupported; import org.eclipse.hono.tests.EnabledIfRegistrySupportsFeatures; import org.eclipse.hono.tests.IntegrationTestSupport; import org.eclipse.hono.tests.Tenants; @@ -297,6 +298,7 @@ public void testUploadQos1MessageFailsIfDeliveryStateNotUpdated( * @throws InterruptedException if the test fails. */ @Test + @EnabledIfDnsRebindingIsSupported @EnabledIfRegistrySupportsFeatures(trustAnchorGroups = true, tenantAlias = true) public void testUploadMessagesUsingClientCertificateWithAlias(final VertxTestContext ctx) throws InterruptedException { @@ -326,7 +328,7 @@ public void testUploadMessagesUsingClientCertificateWithAlias(final VertxTestCon } final RequestOptions options = new RequestOptions() - .setHost("test-alias." + IntegrationTestSupport.HTTP_HOST) + .setHost(IntegrationTestSupport.getSniHostname(IntegrationTestSupport.HTTP_HOST, "test-alias")) .setPort(IntegrationTestSupport.HTTPS_PORT) .setURI(getEndpointUri()) .setHeaders(requestHeaders); diff --git a/tests/src/test/java/org/eclipse/hono/tests/mqtt/MqttConnectionIT.java b/tests/src/test/java/org/eclipse/hono/tests/mqtt/MqttConnectionIT.java index 237aabeb9a..ac5f48c86a 100644 --- a/tests/src/test/java/org/eclipse/hono/tests/mqtt/MqttConnectionIT.java +++ b/tests/src/test/java/org/eclipse/hono/tests/mqtt/MqttConnectionIT.java @@ -29,6 +29,7 @@ import org.eclipse.hono.service.management.credentials.X509CertificateSecret; import org.eclipse.hono.service.management.device.Device; import org.eclipse.hono.service.management.tenant.Tenant; +import org.eclipse.hono.tests.EnabledIfDnsRebindingIsSupported; import org.eclipse.hono.tests.EnabledIfRegistrySupportsFeatures; import org.eclipse.hono.tests.IntegrationTestSupport; import org.eclipse.hono.tests.Tenants; @@ -126,6 +127,7 @@ public void testConnectX509SucceedsForRegisteredDevice(final VertxTestContext ct * @param ctx The test context */ @Test + @EnabledIfDnsRebindingIsSupported @EnabledIfRegistrySupportsFeatures(trustAnchorGroups = true) public void testConnectX509SucceedsUsingSni(final VertxTestContext ctx) { @@ -140,7 +142,9 @@ public void testConnectX509SucceedsUsingSni(final VertxTestContext ctx) { .compose(ok -> helper.registry.addDeviceForTenant(tenantId, tenant, deviceId, cert)); }) // WHEN the device connects to the adapter including its tenant ID in the host name - .compose(ok -> connectToAdapter(deviceCert, tenantId + "." + IntegrationTestSupport.MQTT_HOST)) + .compose(ok -> connectToAdapter( + deviceCert, + IntegrationTestSupport.getSniHostname(IntegrationTestSupport.MQTT_HOST, tenantId))) .onComplete(ctx.succeeding(conAckMsg -> { // THEN the connection attempt succeeds ctx.verify(() -> assertThat(conAckMsg.code()).isEqualTo(MqttConnectReturnCode.CONNECTION_ACCEPTED)); @@ -155,6 +159,7 @@ public void testConnectX509SucceedsUsingSni(final VertxTestContext ctx) { * @param ctx The test context */ @Test + @EnabledIfDnsRebindingIsSupported @EnabledIfRegistrySupportsFeatures(trustAnchorGroups = true, tenantAlias = true) public void testConnectX509SucceedsUsingSniWithTenantAlias(final VertxTestContext ctx) { @@ -174,7 +179,9 @@ public void testConnectX509SucceedsUsingSniWithTenantAlias(final VertxTestContex deviceId, cert)) // WHEN the device connects to the adapter including the tenant alias in the host name - .compose(ok -> connectToAdapter(deviceCert, "test-alias." + IntegrationTestSupport.MQTT_HOST)) + .compose(ok -> connectToAdapter( + deviceCert, + IntegrationTestSupport.getSniHostname(IntegrationTestSupport.MQTT_HOST, "test-alias"))) .onComplete(ctx.succeeding(conAckMsg -> { // THEN the connection attempt succeeds ctx.verify(() -> assertThat(conAckMsg.code()).isEqualTo(MqttConnectReturnCode.CONNECTION_ACCEPTED)); @@ -189,6 +196,7 @@ public void testConnectX509SucceedsUsingSniWithTenantAlias(final VertxTestContex * @param ctx The test context */ @Test + @EnabledIfDnsRebindingIsSupported @EnabledIfRegistrySupportsFeatures(trustAnchorGroups = true, tenantAlias = true) public void testConnectX509FailsUsingSniWithNonExistingTenantAlias(final VertxTestContext ctx) { @@ -208,7 +216,9 @@ public void testConnectX509FailsUsingSniWithNonExistingTenantAlias(final VertxTe deviceId, cert)) // WHEN the device connects to the adapter including a wrong tenant alias in the host name - .compose(ok -> connectToAdapter(deviceCert, "wrong-alias." + IntegrationTestSupport.MQTT_HOST)) + .compose(ok -> connectToAdapter( + deviceCert, + IntegrationTestSupport.getSniHostname(IntegrationTestSupport.MQTT_HOST, "wrong-alias"))) .onComplete(ctx.failing(t -> { // THEN the connection is refused ctx.verify(() -> {