Skip to content

Commit

Permalink
[#3503] Enable clients requesting server time in CoAP responses
Browse files Browse the repository at this point in the history
Added support for letting CoAP devices indicate to the CoAP adapter to include the adapter's local system time in its CoAP response.

For this purpose the device needs to either include the (non-standard) "Time" option in its request or include the "hono-time" request parameter. The adapter will then include its local system time in the response's Time option.

Also added integration tests verifying the behavior.

Signed-off-by: Stefan Freyr Stefansson <[email protected]>
  • Loading branch information
StFS authored Jun 29, 2023
1 parent 8c44c45 commit b5c80fe
Show file tree
Hide file tree
Showing 9 changed files with 389 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public abstract class CoapConstants {
* A tag to use for keeping track of a CoAP message type.
*/
public static final StringTag TAG_COAP_MESSAGE_TYPE = new StringTag("coap.message_type");

/**
* A tag to use for keeping track of a CoAP response code.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.eclipse.californium.core.coap.Response;
import org.eclipse.californium.core.server.resources.CoapExchange;
import org.eclipse.hono.adapter.MapBasedTelemetryExecutionContext;
import org.eclipse.hono.adapter.coap.option.TimeOption;
import org.eclipse.hono.service.auth.DeviceUser;
import org.eclipse.hono.util.CommandConstants;
import org.eclipse.hono.util.Constants;
Expand All @@ -48,6 +49,7 @@ public final class CoapContext extends MapBasedTelemetryExecutionContext {
* The query parameter which is used to indicate an empty notification.
*/
public static final String PARAM_EMPTY_CONTENT = "empty";

/**
* The query parameter which is used to indicate, that a piggypacked response is supported by the device.
* (Legacy support for device with firmware versions not supporting piggypacked response.)
Expand Down Expand Up @@ -398,6 +400,19 @@ public void respondWithCode(final ResponseCode responseCode, final String descri
respond(response);
}

/**
* Checks whether the request includes either the time option or the hono-time parameter which
* indicates that the response should include a time option with the server time.
*
* @return <code>true</code> if response should include the time option containing the server time.
*/
private boolean shouldResponseIncludeTimeOption() {
return Optional.ofNullable(exchange.getRequestOptions())
.map(opts -> opts.hasOption(TimeOption.NUMBER))
.orElse(false)
|| exchange.getQueryParameter(TimeOption.QUERY_PARAMETER_NAME) != null;
}

/**
* Sends a response to the device.
* <p>
Expand All @@ -407,6 +422,11 @@ public void respondWithCode(final ResponseCode responseCode, final String descri
* @return The response code from the response.
*/
public ResponseCode respond(final Response response) {
if (shouldResponseIncludeTimeOption()) {
// Add a time option with the current time to the response
final TimeOption timeOption = new TimeOption();
response.getOptions().addOption(timeOption);
}
acceptFlag.set(true);
exchange.respond(response);
return response.getCode();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Copyright (c) 2023 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.adapter.coap.option;

import org.eclipse.californium.core.coap.Option;

/**
* CoAP custom time option.
* <p>
* Used in CoAP request to indicate that the client wants to get the servers system-time in milliseconds.
* Any value in the option as part of a request is ignored.
* <p>
* If the option is present in a request, the server adds also a time option to the response with the
* servers system-time in milliseconds. Also, a client can request this option be included in a response via the
* {@value #QUERY_PARAMETER_NAME} request parameter.
* <p>
* This option uses the same option number as is used in the Californium cloud-demo-server application
* (<a href="https://github.com/boaks/californium/blob/add_cloud_demo_server/demo-apps/cf-cloud-demo-server/src/main/java/org/eclipse/californium/cloud/option/TimeOption.java#L49">see here</a>).
* TODO: update link once it's been merged into the eclipse-californium project.
*/
public final class TimeOption extends Option {

/**
* The COAP option number.
* <p>
* <b>NOTE:</b> this option number is in the "experimental" range and as such is not suitable for
* interoperability with other CoAP implementations. This implementation should be changed if CoAP ever
* defines its own official option number for reporting server time.
* <p>
* For further information and discussion, see:
* <ul>
* <li> <a href="https://www.iana.org/assignments/core-parameters/core-parameters.xhtml#option-numbers">IANA CoAP Option Numbers</a> </li>
* <li> <a href="https://github.com/eclipse-californium/californium/issues/2134">Question about server time reporting in Californium</a> </li>
* <li> <a href="https://github.com/eclipse-hono/hono/issues/3502">Issue for adding a time option to Hono</a> </li>
* <li> <a href="https://github.com/eclipse-hono/hono/pull/3503">Pull request for adding a time option to Hono</a> </li>
* </ul>
*/
public static final int NUMBER = 0xfde8;

/**
* The request parameter name clients should use to request the server <em>time</em> be sent back as part of the
* response (as a time option).
*/
public static final String QUERY_PARAMETER_NAME = "hono-time";

/**
* Create time option with current system time.
*/
public TimeOption() {
super(NUMBER, System.currentTimeMillis());
}

/**
* Create time option.
*
* @param time time in system milliseconds.
*/
public TimeOption(final long time) {
super(NUMBER, time);
}

/**
* Create time option.
*
* @param value time in system milliseconds as byte array.
*/
public TimeOption(final byte[] value) {
super(NUMBER, value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import static com.google.common.truth.Truth.assertThat;

import org.eclipse.californium.core.coap.Option;
import org.eclipse.californium.core.coap.OptionSet;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.californium.core.server.resources.CoapExchange;
import org.eclipse.hono.adapter.coap.option.TimeOption;
import org.eclipse.hono.service.auth.DeviceUser;
import org.eclipse.hono.test.TracingMockSupport;
import org.eclipse.hono.test.VertxMockSupport;
Expand All @@ -33,10 +39,11 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import com.google.common.collect.Range;

import io.opentracing.Span;
import io.vertx.core.Vertx;


/**
* Tests verifying behavior of {@link CoapContext}.
*
Expand Down Expand Up @@ -162,4 +169,70 @@ void testStartAckTimerHandlesNonNumberPropertyValue() {
ctx.startAcceptTimer(vertx, tenant, 500);
verify(vertx).setTimer(eq(500L), VertxMockSupport.anyHandler());
}

/**
* Verifies that the CoAP time option is not in the response if not requested by either a request option or parameter.
*/
@Test
void testTimeOptionIsNotIncludedInResponseIfNotRequested() {
final CoapExchange exchange = mock(CoapExchange.class);
when(exchange.getRequestOptions()).thenReturn(new OptionSet());
final Adapter coapConfig = new Adapter(Constants.PROTOCOL_ADAPTER_TYPE_COAP);
final TenantObject tenant = TenantObject.from("tenant", true).addAdapter(coapConfig);
final var authenticatedDevice = new DeviceUser(tenant.getTenantId(), "device-id");
final CoapContext ctx = CoapContext.fromRequest(exchange, authenticatedDevice, authenticatedDevice, "4711", span);
final Response response = mock(Response.class);
final OptionSet responseOptions = new OptionSet();
when(response.getOptions()).thenReturn(responseOptions);
ctx.respond(response);
assertThat(responseOptions.hasOption(TimeOption.NUMBER)).isFalse();
}

/**
* Verifies that the CoAP time option is in the response if requested by a request option.
*/
@Test
void testTimeOptionIsIncludedInResponseIfOptionPresentInRequest() {
final long start = System.currentTimeMillis();
final CoapExchange exchange = mock(CoapExchange.class);
final OptionSet requestOptions = new OptionSet();
requestOptions.addOption(new Option(TimeOption.NUMBER, new byte[0]));
when(exchange.getRequestOptions()).thenReturn(requestOptions);
final Adapter coapConfig = new Adapter(Constants.PROTOCOL_ADAPTER_TYPE_COAP);
final TenantObject tenant = TenantObject.from("tenant", true).addAdapter(coapConfig);
final var authenticatedDevice = new DeviceUser(tenant.getTenantId(), "device-id");
final CoapContext ctx = CoapContext.fromRequest(exchange, authenticatedDevice, authenticatedDevice, "4711", span);
final Response response = mock(Response.class);
final OptionSet responseOptions = new OptionSet();
when(response.getOptions()).thenReturn(responseOptions);
ctx.respond(response);
verify(response).getOptions();
assertThat(responseOptions.hasOption(TimeOption.NUMBER)).isTrue();
final long serverTime = responseOptions.getOtherOption(TimeOption.NUMBER).getLongValue();
final long end = System.currentTimeMillis();
assertThat(serverTime).isIn(Range.closed(start, end));
}

/**
* Verifies that the CoAP time option is in the response if requested by a request parameter.
*/
@Test
void testTimeOptionIsIncludedInResponseIfParameterPresentInRequest() {
final long start = System.currentTimeMillis();
final CoapExchange exchange = mock(CoapExchange.class);
when(exchange.getQueryParameter(eq(TimeOption.QUERY_PARAMETER_NAME))).thenReturn("true");
final Adapter coapConfig = new Adapter(Constants.PROTOCOL_ADAPTER_TYPE_COAP);
final TenantObject tenant = TenantObject.from("tenant", true).addAdapter(coapConfig);
final var authenticatedDevice = new DeviceUser(tenant.getTenantId(), "device-id");
final CoapContext ctx = CoapContext.fromRequest(exchange, authenticatedDevice, authenticatedDevice, "4711", span);
final Response response = mock(Response.class);
final OptionSet responseOptions = new OptionSet();
when(response.getOptions()).thenReturn(responseOptions);
ctx.respond(response);
verify(response).getOptions();
assertThat(responseOptions.hasOption(TimeOption.NUMBER)).isTrue();
final long serverTime = responseOptions.getOtherOption(TimeOption.NUMBER).getLongValue();
final long end = System.currentTimeMillis();
assertThat(serverTime).isIn(Range.closed(start, end));
}
}
5 changes: 5 additions & 0 deletions bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,11 @@ quarkus.vertx.max-event-loop-execute-time=${max.event-loop.execute-time:20000}
<artifactId>hono-adapter-base</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.hono</groupId>
<artifactId>hono-adapter-coap</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.hono</groupId>
<artifactId>hono-service-base</artifactId>
Expand Down
1 change: 1 addition & 0 deletions legal/src/main/resources/legal/NOTICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ of the Eclipse Public License 2.0 which is available at https://www.eclipse.org/
* Copyright 2020 pragmatic industries GmbH
* Copyright 2020 Lari Hotari
* Copyright 2022-2023 SOTEC GmbH & Co KG
* Copyright 2023 Controlant ehf.

# Third-party Content

Expand Down
Loading

0 comments on commit b5c80fe

Please sign in to comment.