From 0d24d6d9044beed86085bba6558797960876d630 Mon Sep 17 00:00:00 2001 From: Simon Bernard Date: Wed, 16 Jun 2021 17:02:28 +0200 Subject: [PATCH] #1000: Add a BootstrapConsistencyChecker to Leshan Client. --- .../client/californium/LeshanClient.java | 16 +- .../californium/LeshanClientBuilder.java | 35 +++- .../BootstrapConsistencyChecker.java | 36 ++++ .../client/bootstrap/BootstrapHandler.java | 32 +++- .../DefaultBootstrapConsistencyChecker.java | 45 +++++ .../bootstrap/InvalidStateException.java | 47 ++++++ .../engine/DefaultRegistrationEngine.java | 41 +++-- .../client/servers/ServersInfoExtractor.java | 158 ++++++++++-------- .../integration/tests/BootstrapTest.java | 26 +++ .../util/BootstrapIntegrationTestHelper.java | 15 +- .../tests/util/IntegrationTestHelper.java | 8 + .../tests/util/SynchronousClientObserver.java | 31 +++- 12 files changed, 382 insertions(+), 108 deletions(-) create mode 100644 leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/BootstrapConsistencyChecker.java create mode 100644 leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/DefaultBootstrapConsistencyChecker.java create mode 100644 leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/InvalidStateException.java diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClient.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClient.java index 43d7d5ba1f..1b7ccce207 100644 --- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClient.java +++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClient.java @@ -33,7 +33,9 @@ import org.eclipse.californium.scandium.config.DtlsConnectorConfig.Builder; import org.eclipse.leshan.client.LwM2mClient; import org.eclipse.leshan.client.RegistrationUpdateHandler; +import org.eclipse.leshan.client.bootstrap.BootstrapConsistencyChecker; import org.eclipse.leshan.client.bootstrap.BootstrapHandler; +import org.eclipse.leshan.client.bootstrap.DefaultBootstrapConsistencyChecker; import org.eclipse.leshan.client.californium.bootstrap.BootstrapResource; import org.eclipse.leshan.client.californium.object.ObjectResource; import org.eclipse.leshan.client.californium.request.CaliforniumLwM2mRequestSender; @@ -104,15 +106,17 @@ public LeshanClient(String endpoint, InetSocketAddress localAddress, Map additionalAttributes, Map bsAdditionalAttributes, LwM2mNodeEncoder encoder, LwM2mNodeDecoder decoder, ScheduledExecutorService sharedExecutor) { this(endpoint, localAddress, objectEnablers, coapConfig, dtlsConfigBuilder, null, endpointFactory, - engineFactory, additionalAttributes, bsAdditionalAttributes, encoder, decoder, sharedExecutor); + engineFactory, new DefaultBootstrapConsistencyChecker(), additionalAttributes, bsAdditionalAttributes, + encoder, decoder, sharedExecutor); } /** @since 2.0 */ public LeshanClient(String endpoint, InetSocketAddress localAddress, List objectEnablers, NetworkConfig coapConfig, Builder dtlsConfigBuilder, List trustStore, EndpointFactory endpointFactory, RegistrationEngineFactory engineFactory, - Map additionalAttributes, Map bsAdditionalAttributes, - LwM2mNodeEncoder encoder, LwM2mNodeDecoder decoder, ScheduledExecutorService sharedExecutor) { + BootstrapConsistencyChecker checker, Map additionalAttributes, + Map bsAdditionalAttributes, LwM2mNodeEncoder encoder, LwM2mNodeDecoder decoder, + ScheduledExecutorService sharedExecutor) { Validate.notNull(endpoint); Validate.notEmpty(objectEnablers); @@ -123,7 +127,7 @@ public LeshanClient(String endpoint, InetSocketAddress localAddress, this.decoder = decoder; this.encoder = encoder; observers = createClientObserverDispatcher(); - bootstrapHandler = createBoostrapHandler(objectTree); + bootstrapHandler = createBoostrapHandler(objectTree, checker); endpointsManager = createEndpointsManager(localAddress, coapConfig, dtlsConfigBuilder, trustStore, endpointFactory); requestSender = createRequestSender(endpointsManager, sharedExecutor, encoder, objectTree.getModel()); @@ -160,8 +164,8 @@ public void onUnexpectedError(Throwable unexpectedError) { return observer; } - protected BootstrapHandler createBoostrapHandler(LwM2mObjectTree objectTree) { - return new BootstrapHandler(objectTree.getObjectEnablers()); + protected BootstrapHandler createBoostrapHandler(LwM2mObjectTree objectTree, BootstrapConsistencyChecker checker) { + return new BootstrapHandler(objectTree.getObjectEnablers(), checker); } protected CoapServer createCoapServer(NetworkConfig coapConfig, ScheduledExecutorService sharedExecutor) { diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClientBuilder.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClientBuilder.java index be06ab0287..aea71ed051 100644 --- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClientBuilder.java +++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClientBuilder.java @@ -31,6 +31,8 @@ import org.eclipse.californium.scandium.DTLSConnector; import org.eclipse.californium.scandium.config.DtlsConnectorConfig; import org.eclipse.californium.scandium.config.DtlsConnectorConfig.Builder; +import org.eclipse.leshan.client.bootstrap.BootstrapConsistencyChecker; +import org.eclipse.leshan.client.bootstrap.DefaultBootstrapConsistencyChecker; import org.eclipse.leshan.client.engine.DefaultRegistrationEngineFactory; import org.eclipse.leshan.client.engine.RegistrationEngine; import org.eclipse.leshan.client.engine.RegistrationEngineFactory; @@ -71,11 +73,11 @@ public class LeshanClientBuilder { private EndpointFactory endpointFactory; private RegistrationEngineFactory engineFactory; private Map additionalAttributes; + private Map bsAdditionalAttributes; - private ScheduledExecutorService executor; + private BootstrapConsistencyChecker bootstrapConsistencyChecker; - /** @since 1.1 */ - protected Map bsAdditionalAttributes; + private ScheduledExecutorService executor; /** * Creates a new instance for setting the configuration options for a {@link LeshanClient} instance. @@ -217,6 +219,18 @@ public LeshanClientBuilder setBootstrapAdditionalAttributes(Map return this; } + /** + * Set a {@link BootstrapConsistencyChecker} which is used to valid client state after a bootstrap session. + *

+ * By default a {@link DefaultBootstrapConsistencyChecker} is used. + * + * @return the builder for fluent client creation. + */ + public LeshanClientBuilder setBootstrapConsistencyChecker(BootstrapConsistencyChecker checker) { + this.bootstrapConsistencyChecker = checker; + return this; + } + /** * Set a shared executor. This executor will be used everywhere it is possible. This is generally used when you want * to limit the number of thread to use or if you want to simulate a lot of clients sharing the same thread pool. @@ -283,6 +297,9 @@ protected Connector createSecuredConnector(DtlsConnectorConfig dtlsConfig) { } }; } + if (bootstrapConsistencyChecker == null) { + bootstrapConsistencyChecker = new DefaultBootstrapConsistencyChecker(); + } // handle dtlsConfig if (dtlsConfigBuilder == null) { @@ -324,7 +341,8 @@ protected Connector createSecuredConnector(DtlsConnectorConfig dtlsConfig) { } return createLeshanClient(endpoint, localAddress, objectEnablers, coapConfig, dtlsConfigBuilder, - this.trustStore, endpointFactory, engineFactory, additionalAttributes, encoder, decoder, executor); + this.trustStore, endpointFactory, engineFactory, bootstrapConsistencyChecker, additionalAttributes, + bsAdditionalAttributes, encoder, decoder, executor); } /** @@ -344,7 +362,9 @@ protected Connector createSecuredConnector(DtlsConnectorConfig dtlsConfig) { * @param trustStore The optional trust store for verifying X.509 server certificates. * @param endpointFactory The factory which will create the {@link CoapEndpoint}. * @param engineFactory The factory which will create the {@link RegistrationEngine}. + * @param checker Used to check if client state is consistent after a bootstrap session. * @param additionalAttributes Some extra (out-of-spec) attributes to add to the register request. + * @param bsAdditionalAttributes Some extra (out-of-spec) attributes to add to the bootstrap request. * @param encoder used to encode request payload. * @param decoder used to decode response payload. * @param sharedExecutor an optional shared executor. @@ -354,10 +374,11 @@ protected Connector createSecuredConnector(DtlsConnectorConfig dtlsConfig) { protected LeshanClient createLeshanClient(String endpoint, InetSocketAddress localAddress, List objectEnablers, NetworkConfig coapConfig, Builder dtlsConfigBuilder, List trustStore, EndpointFactory endpointFactory, RegistrationEngineFactory engineFactory, - Map additionalAttributes, LwM2mNodeEncoder encoder, LwM2mNodeDecoder decoder, + BootstrapConsistencyChecker checker, Map additionalAttributes, + Map bsAdditionalAttributes, LwM2mNodeEncoder encoder, LwM2mNodeDecoder decoder, ScheduledExecutorService sharedExecutor) { return new LeshanClient(endpoint, localAddress, objectEnablers, coapConfig, dtlsConfigBuilder, trustStore, - endpointFactory, engineFactory, additionalAttributes, bsAdditionalAttributes, encoder, decoder, - executor); + endpointFactory, engineFactory, checker, additionalAttributes, bsAdditionalAttributes, encoder, decoder, + sharedExecutor); } } diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/BootstrapConsistencyChecker.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/BootstrapConsistencyChecker.java new file mode 100644 index 0000000000..6957c4e73d --- /dev/null +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/BootstrapConsistencyChecker.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2021 Sierra Wireless and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.html. + * + * Contributors: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.client.bootstrap; + +import java.util.List; +import java.util.Map; + +import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; + +/** + * This class is responsible to check the consistency state of the client after at the end of a bootstrap session. + */ +public interface BootstrapConsistencyChecker { + + /** + * check if current state of the client is consistent + * + * @param objectEnablers all objects supported by the client. + * @return null if the current state is consistent or a list of issues + */ + List checkconfig(Map objectEnablers); + +} diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/BootstrapHandler.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/BootstrapHandler.java index 80ced31503..bdfa846e51 100644 --- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/BootstrapHandler.java +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/BootstrapHandler.java @@ -16,6 +16,7 @@ *******************************************************************************/ package org.eclipse.leshan.client.bootstrap; +import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -40,11 +41,15 @@ public class BootstrapHandler { private boolean bootstrapping = false; private CountDownLatch bootstrappingLatch = new CountDownLatch(1); + // last session state (null means no error) + private volatile List lastConsistencyError = null; private final Map objects; + private BootstrapConsistencyChecker checker; - public BootstrapHandler(Map objectEnablers) { + public BootstrapHandler(Map objectEnablers, BootstrapConsistencyChecker checker) { objects = objectEnablers; + this.checker = checker; } public synchronized SendableResponse finished(ServerIdentity server, @@ -54,7 +59,6 @@ public synchronized SendableResponse finished(ServerIde if (!server.isLwm2mBootstrapServer()) { return new SendableResponse<>(BootstrapFinishResponse.badRequest("not from a bootstrap server")); } - // TODO delete bootstrap server (see 5.2.5.2 Bootstrap Delete) Runnable whenSent = new Runnable() { @Override @@ -63,7 +67,17 @@ public void run() { } }; - return new SendableResponse<>(BootstrapFinishResponse.success(), whenSent); + // check consistency state of the client + lastConsistencyError = checker.checkconfig(objects); + if (lastConsistencyError == null) { + return new SendableResponse<>(BootstrapFinishResponse.success(), whenSent); + } else { + // TODO rollback configuration. + // see https://github.com/eclipse/leshan/issues/968 + return new SendableResponse<>(BootstrapFinishResponse.notAcceptable(lastConsistencyError.toString()), + whenSent); + } + } else { return new SendableResponse<>(BootstrapFinishResponse.badRequest("no pending bootstrap session")); } @@ -91,6 +105,7 @@ public synchronized boolean tryToInitSession() { if (!bootstrapping) { bootstrappingLatch = new CountDownLatch(1); bootstrapping = true; + lastConsistencyError = null; return true; } return false; @@ -100,8 +115,15 @@ public synchronized boolean isBootstrapping() { return bootstrapping; } - public boolean waitBoostrapFinished(long timeInSeconds) throws InterruptedException { - return bootstrappingLatch.await(timeInSeconds, TimeUnit.SECONDS); + public boolean waitBoostrapFinished(long timeInSeconds) throws InterruptedException, InvalidStateException { + boolean finished = bootstrappingLatch.await(timeInSeconds, TimeUnit.SECONDS); + if (finished) { + if (lastConsistencyError != null) { + throw new InvalidStateException( + String.format("Invalid Bootstrap state : %s", lastConsistencyError.toString())); + } + } + return finished; } public synchronized void closeSession() { diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/DefaultBootstrapConsistencyChecker.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/DefaultBootstrapConsistencyChecker.java new file mode 100644 index 0000000000..d1546b0f11 --- /dev/null +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/DefaultBootstrapConsistencyChecker.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) 2021 Sierra Wireless and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.html. + * + * Contributors: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.client.bootstrap; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; +import org.eclipse.leshan.client.servers.ServersInfoExtractor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A default implementation of {@link BootstrapConsistencyChecker} + */ +public class DefaultBootstrapConsistencyChecker implements BootstrapConsistencyChecker { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultBootstrapConsistencyChecker.class); + + @Override + public List checkconfig(Map objectEnablers) { + try { + ServersInfoExtractor.getInfo(objectEnablers, true); + } catch (RuntimeException e) { + LOG.debug(e.getMessage()); + return Arrays.asList(e.getMessage()); + } + return null; + } + +} diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/InvalidStateException.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/InvalidStateException.java new file mode 100644 index 0000000000..dc64802e35 --- /dev/null +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/InvalidStateException.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright (c) 2021 Sierra Wireless and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.html. + * + * Contributors: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.client.bootstrap; + +/** + * Raised if LWM2M client is in an invalid or inconsistent state. + * + */ +public class InvalidStateException extends Exception { + private static final long serialVersionUID = 1L; + + public InvalidStateException() { + } + + public InvalidStateException(String m) { + super(m); + } + + public InvalidStateException(String m, Object... args) { + super(String.format(m, args)); + } + + public InvalidStateException(Throwable e) { + super(e); + } + + public InvalidStateException(String m, Throwable e) { + super(m, e); + } + + public InvalidStateException(Throwable e, String m, Object... args) { + super(String.format(m, args), e); + } +} diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/DefaultRegistrationEngine.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/DefaultRegistrationEngine.java index 16e2ed2788..4b52e2b414 100644 --- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/DefaultRegistrationEngine.java +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/DefaultRegistrationEngine.java @@ -33,6 +33,7 @@ import org.eclipse.leshan.client.EndpointsManager; import org.eclipse.leshan.client.RegistrationUpdate; import org.eclipse.leshan.client.bootstrap.BootstrapHandler; +import org.eclipse.leshan.client.bootstrap.InvalidStateException; import org.eclipse.leshan.client.observer.LwM2mClientObserver; import org.eclipse.leshan.client.request.LwM2mRequestSender; import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; @@ -233,25 +234,33 @@ private ServerIdentity clientInitiatedBootstrap() throws InterruptedException { } else if (response.isSuccess()) { LOG.info("Bootstrap started"); // Wait until it is finished (or too late) - boolean timeout = !bootstrapHandler.waitBoostrapFinished(bootstrapSessionTimeoutInSec); - if (timeout) { - LOG.info("Bootstrap sequence aborted: Timeout."); - if (observer != null) { - observer.onBootstrapTimeout(bootstrapServer, request); - } - return null; - } else { - LOG.info("Bootstrap finished {}.", bootstrapServer.getUri()); - ServerInfo serverInfo = selectServer( - ServersInfoExtractor.getInfo(objectEnablers).deviceManagements); - ServerIdentity dmServer = null; - if (serverInfo != null) { - dmServer = endpointsManager.createEndpoint(serverInfo); + try { + boolean timeout = !bootstrapHandler.waitBoostrapFinished(bootstrapSessionTimeoutInSec); + if (timeout) { + LOG.info("Bootstrap sequence aborted: Timeout."); + if (observer != null) { + observer.onBootstrapTimeout(bootstrapServer, request); + } + return null; + } else { + LOG.info("Bootstrap finished {}.", bootstrapServer.getUri()); + ServerInfo serverInfo = selectServer( + ServersInfoExtractor.getInfo(objectEnablers).deviceManagements); + ServerIdentity dmServer = null; + if (serverInfo != null) { + dmServer = endpointsManager.createEndpoint(serverInfo); + } + if (observer != null) { + observer.onBootstrapSuccess(bootstrapServer, request); + } + return dmServer; } + } catch (InvalidStateException e) { + LOG.info("Bootstrap finished with failure because of consistency check failure.", e); if (observer != null) { - observer.onBootstrapSuccess(bootstrapServer, request); + observer.onBootstrapFailure(bootstrapServer, request, null, null, e); } - return dmServer; + return null; } } else { LOG.info("Bootstrap failed: {} {}.", response.getCode(), response.getErrorMessage()); diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/servers/ServersInfoExtractor.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/servers/ServersInfoExtractor.java index 8b109b6a05..3a455ceca0 100644 --- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/servers/ServersInfoExtractor.java +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/servers/ServersInfoExtractor.java @@ -59,6 +59,12 @@ public class ServersInfoExtractor { private static final Logger LOG = LoggerFactory.getLogger(ServersInfoExtractor.class); public static ServersInfo getInfo(Map objectEnablers) { + return getInfo(objectEnablers, false); + } + + public static ServersInfo getInfo(Map objectEnablers, boolean raiseException) + throws IllegalStateException { + LwM2mObjectEnabler securityEnabler = objectEnablers.get(SECURITY); LwM2mObjectEnabler serverEnabler = objectEnablers.get(SERVER); @@ -70,74 +76,91 @@ public static ServersInfo getInfo(Map objectEnabler LwM2mObject servers = (LwM2mObject) serverEnabler.read(SYSTEM, new ReadRequest(SERVER)).getContent(); for (LwM2mObjectInstance security : securities.getInstances().values()) { - try { - if ((boolean) security.getResource(SEC_BOOTSTRAP).getValue()) { - if (infos.bootstrap != null) { - LOG.warn("There is more than one bootstrap configuration in security object."); - } else { - // create bootstrap info - ServerInfo info = new ServerInfo(); - info.bootstrap = true; - LwM2mResource serverIdResource = security.getResource(SEC_SERVER_ID); - if (serverIdResource != null && serverIdResource.getValue() != null) - info.serverId = (long) serverIdResource.getValue(); - else - info.serverId = 0; - info.serverUri = new URI((String) security.getResource(SEC_SERVER_URI).getValue()); - info.secureMode = getSecurityMode(security); - if (info.secureMode == SecurityMode.PSK) { - info.pskId = getPskIdentity(security); - info.pskKey = getPskKey(security); - } else if (info.secureMode == SecurityMode.RPK) { - info.publicKey = getPublicKey(security); - info.privateKey = getPrivateKey(security); - info.serverPublicKey = getServerPublicKey(security); - } else if (info.secureMode == SecurityMode.X509) { - info.clientCertificate = getClientCertificate(security); - info.serverCertificate = getServerCertificate(security); - info.privateKey = getPrivateKey(security); - info.certificateUsage = getCertificateUsage(security); - } - infos.bootstrap = info; + if ((boolean) security.getResource(SEC_BOOTSTRAP).getValue()) { + if (infos.bootstrap != null) { + String message = "There is more than one bootstrap configuration in security object."; + LOG.debug(message); + if (raiseException) { + throw new IllegalStateException(message); } } else { - // create device management info - DmServerInfo info = new DmServerInfo(); - info.bootstrap = false; - info.serverUri = new URI((String) security.getResource(SEC_SERVER_URI).getValue()); - info.serverId = (long) security.getResource(SEC_SERVER_ID).getValue(); - info.secureMode = getSecurityMode(security); - if (info.secureMode == SecurityMode.PSK) { - info.pskId = getPskIdentity(security); - info.pskKey = getPskKey(security); - } else if (info.secureMode == SecurityMode.RPK) { - info.publicKey = getPublicKey(security); - info.privateKey = getPrivateKey(security); - info.serverPublicKey = getServerPublicKey(security); - } else if (info.secureMode == SecurityMode.X509) { - info.clientCertificate = getClientCertificate(security); - info.serverCertificate = getServerCertificate(security); - info.privateKey = getPrivateKey(security); - info.certificateUsage = getCertificateUsage(security); - } - // search corresponding device management server - for (LwM2mObjectInstance server : servers.getInstances().values()) { - if (info.serverId == (Long) server.getResource(SRV_SERVER_ID).getValue()) { - info.lifetime = (long) server.getResource(SRV_LIFETIME).getValue(); - info.binding = BindingMode.parse((String) server.getResource(SRV_BINDING).getValue()); - - infos.deviceManagements.put(info.serverId, info); - break; - } + // create server info for bootstrap server + ServerInfo serverInfo = new ServerInfo(); + serverInfo.bootstrap = true; + try { + // fill info from current client state. + populateServerInfo(serverInfo, security); + + // add server info to result to return + infos.bootstrap = serverInfo; + } catch (RuntimeException e) { + LOG.debug("Unable to get info for bootstrap server /O/{}", security.getId(), e); + if (raiseException) + throw e; } } - } catch (URISyntaxException e) { - LOG.error(String.format("Invalid URI %s", (String) security.getResource(SEC_SERVER_URI).getValue()), e); + } else { + try { + // create device management info + DmServerInfo info = createDMServerInfo(security, servers); + infos.deviceManagements.put(info.serverId, info); + } catch (RuntimeException e) { + LOG.debug("Unable to get info for DM server /O/{}", security.getId(), e); + if (raiseException) + throw e; + } } } return infos; } + private static void populateServerInfo(ServerInfo info, LwM2mObjectInstance security) { + try { + LwM2mResource serverIdResource = security.getResource(SEC_SERVER_ID); + if (serverIdResource != null && serverIdResource.getValue() != null) + info.serverId = (long) serverIdResource.getValue(); + else + info.serverId = 0; + info.serverUri = new URI((String) security.getResource(SEC_SERVER_URI).getValue()); + info.secureMode = getSecurityMode(security); + if (info.secureMode == SecurityMode.PSK) { + info.pskId = getPskIdentity(security); + info.pskKey = getPskKey(security); + } else if (info.secureMode == SecurityMode.RPK) { + info.publicKey = getPublicKey(security); + info.privateKey = getPrivateKey(security); + info.serverPublicKey = getServerPublicKey(security); + } else if (info.secureMode == SecurityMode.X509) { + info.clientCertificate = getClientCertificate(security); + info.serverCertificate = getServerCertificate(security); + info.privateKey = getPrivateKey(security); + info.certificateUsage = getCertificateUsage(security); + } + } catch (RuntimeException | URISyntaxException e) { + throw new IllegalStateException("Invalid Security Instance /0/" + security.getId(), e); + } + } + + private static DmServerInfo createDMServerInfo(LwM2mObjectInstance security, LwM2mObject servers) { + DmServerInfo info = new DmServerInfo(); + info.bootstrap = false; + populateServerInfo(info, security); + + // search corresponding device management server + for (LwM2mObjectInstance server : servers.getInstances().values()) { + try { + if (info.serverId == (Long) server.getResource(SRV_SERVER_ID).getValue()) { + info.lifetime = (long) server.getResource(SRV_LIFETIME).getValue(); + info.binding = BindingMode.parse((String) server.getResource(SRV_BINDING).getValue()); + return info; + } + } catch (RuntimeException e) { + throw new IllegalStateException("Invalid Server Instance /1/" + server.getId(), e); + } + } + return null; + } + public static DmServerInfo getDMServerInfo(Map objectEnablers, Long shortID) { ServersInfo info = getInfo(objectEnablers); if (info == null) @@ -245,11 +268,10 @@ private static PublicKey getPublicKey(LwM2mObjectInstance securityInstance) { KeyFactory kf = KeyFactory.getInstance(algorithm); return kf.generatePublic(keySpec); } catch (NoSuchAlgorithmException e) { - LOG.debug("Failed to instantiate key factory for algorithm " + algorithm, e); + throw new IllegalArgumentException("Failed to instantiate key factory for algorithm " + algorithm, e); } catch (InvalidKeySpecException e) { - LOG.debug("Failed to decode RFC7250 public key with algorithm " + algorithm, e); + throw new IllegalArgumentException("Failed to decode RFC7250 public key with algorithm " + algorithm, e); } - return null; } private static PrivateKey getPrivateKey(LwM2mObjectInstance securityInstance) { @@ -257,8 +279,7 @@ private static PrivateKey getPrivateKey(LwM2mObjectInstance securityInstance) { try { return SecurityUtil.privateKey.decode(encodedKey); } catch (IOException | GeneralSecurityException e) { - LOG.debug("Failed to decode RFC5958 private key", e); - return null; + throw new IllegalArgumentException("Failed to decode RFC5958 private key", e); } } @@ -267,8 +288,7 @@ private static PublicKey getServerPublicKey(LwM2mObjectInstance securityInstance try { return SecurityUtil.publicKey.decode(encodedKey); } catch (IOException | GeneralSecurityException e) { - LOG.debug("Failed to decode RFC7250 public key", e); - return null; + throw new IllegalArgumentException("Failed to decode RFC7250 public key", e); } } @@ -277,8 +297,7 @@ private static Certificate getServerCertificate(LwM2mObjectInstance securityInst try { return SecurityUtil.certificate.decode(encodedCert); } catch (IOException | GeneralSecurityException e) { - LOG.debug("Failed to decode X.509 certificate", e); - return null; + throw new IllegalArgumentException("Failed to decode X.509 certificate", e); } } @@ -290,8 +309,7 @@ private static Certificate getClientCertificate(LwM2mObjectInstance securityInst return cf.generateCertificate(in); } } catch (CertificateException | IOException e) { - LOG.debug("Failed to decode X.509 certificate", e); - return null; + throw new IllegalArgumentException("Failed to decode X.509 certificate", e); } } diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapTest.java index 0babe7eec1..561c09eef4 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapTest.java @@ -273,6 +273,32 @@ public void bootstrapWithDiscoverOnDevice() { assertEquals(";lwm2m=1.0,;ver=1.1,", Link.serialize(helper.lastDiscoverAnswer.getObjectLinks())); } + @Test + public void bootstrap_create_2_bsserver() { + // Create DM Server without security & start it + helper.createServer(); + helper.server.start(); + + // Create and start bootstrap server + helper.createBootstrapServer(null, helper.unsecuredBootstrapStoreWithBsSecurityInstanceIdAt(0)); + helper.bootstrapServer.start(); + + // Create Client with bootstrap server config at /0/10 + helper.createClient(helper.withoutSecurityAndInstanceId(10), null); + helper.assertClientNotRegisterered(); + + // Start BS. + // Server will delete /0 but Client will not delete /0/10 instance (because bs server is not deletable) + // Then a new BS server will be written at /0/0 + // + // So bootstrap should failed because 2 bs server in Security Object is not a valid state. + // see https://github.com/OpenMobileAlliance/OMA_LwM2M_for_Developers/issues/523 + helper.client.start(); + + // ensure bootstrap session failed because of invalid state + helper.waitForInconsistentStateAtClientSide(1); + } + @Test public void bootstrapDeleteSecurity() { // Create DM Server without security & start it diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/BootstrapIntegrationTestHelper.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/BootstrapIntegrationTestHelper.java index 58bd250c08..53b1d8cc56 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/BootstrapIntegrationTestHelper.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/BootstrapIntegrationTestHelper.java @@ -203,10 +203,17 @@ public void createBootstrapServer(BootstrapSecurityStore securityStore) { } public Security withoutSecurity() { + return withoutSecurityAndInstanceId(null); + } + + public Security withoutSecurityAndInstanceId(Integer id) { // Create Security Object (with bootstrap server only) String bsUrl = "coap://" + bootstrapServer.getUnsecuredAddress().getHostString() + ":" + bootstrapServer.getUnsecuredAddress().getPort(); - return Security.noSecBootstap(bsUrl); + Security sec = Security.noSecBootstap(bsUrl); + if (id != null) + sec.setId(id); + return sec; } @Override @@ -348,6 +355,10 @@ public Iterator getAllByEndpoint(String endpoint) { } public BootstrapConfigStore unsecuredBootstrapStore() { + return unsecuredBootstrapStoreWithBsSecurityInstanceIdAt(0); + } + + public BootstrapConfigStore unsecuredBootstrapStoreWithBsSecurityInstanceIdAt(final int instanceId) { return new BootstrapConfigStore() { @Override @@ -361,7 +372,7 @@ public BootstrapConfig get(String endpoint, Identity deviceIdentity, BootstrapSe bsSecurity.uri = "coap://" + bootstrapServer.getUnsecuredAddress().getHostString() + ":" + bootstrapServer.getUnsecuredAddress().getPort(); bsSecurity.securityMode = SecurityMode.NO_SEC; - bsConfig.security.put(0, bsSecurity); + bsConfig.security.put(instanceId, bsSecurity); // security for DM server ServerSecurity dmSecurity = new ServerSecurity(); diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/IntegrationTestHelper.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/IntegrationTestHelper.java index d505360c10..dac832e281 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/IntegrationTestHelper.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/IntegrationTestHelper.java @@ -281,6 +281,14 @@ public void waitForBootstrapFinishedAtClientSide(long timeInSeconds) { } } + public void waitForInconsistentStateAtClientSide(long timeInSeconds) { + try { + assertTrue(clientObserver.waitForInconsistenState(timeInSeconds, TimeUnit.SECONDS)); + } catch (InterruptedException | TimeoutException e) { + throw new RuntimeException(e); + } + } + public void ensureNoUpdate(long timeInSeconds) { try { registrationListener.waitForUpdate(timeInSeconds, TimeUnit.SECONDS); diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/SynchronousClientObserver.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/SynchronousClientObserver.java index 79e232ab81..cd46e67d9f 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/SynchronousClientObserver.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/SynchronousClientObserver.java @@ -20,6 +20,7 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; +import org.eclipse.leshan.client.bootstrap.InvalidStateException; import org.eclipse.leshan.client.observer.LwM2mClientObserverAdapter; import org.eclipse.leshan.client.servers.ServerIdentity; import org.eclipse.leshan.core.ResponseCode; @@ -44,6 +45,7 @@ public class SynchronousClientObserver extends LwM2mClientObserverAdapter { private CountDownLatch bootstrapLatch = new CountDownLatch(1); private AtomicBoolean bootstrapSucceed = new AtomicBoolean(false); + private AtomicBoolean bootstrapInvalidState = new AtomicBoolean(false); private AtomicBoolean bootstrapFailed = new AtomicBoolean(false); @Override @@ -55,6 +57,9 @@ public void onBootstrapSuccess(ServerIdentity bsserver, BootstrapRequest request @Override public void onBootstrapFailure(ServerIdentity bsserver, BootstrapRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + if (cause instanceof InvalidStateException) { + bootstrapInvalidState.set(true); + } bootstrapFailed.set(true); bootstrapLatch.countDown(); } @@ -89,8 +94,8 @@ public void onUpdateSuccess(ServerIdentity server, UpdateRequest request) { } @Override - public void onUpdateFailure(ServerIdentity server, UpdateRequest request, ResponseCode responseCode, String errorMessage, - Exception cause) { + public void onUpdateFailure(ServerIdentity server, UpdateRequest request, ResponseCode responseCode, + String errorMessage, Exception cause) { updateFailed.set(true); updateLatch.countDown(); } @@ -202,4 +207,26 @@ public boolean waitForBootstrap(long timeout, TimeUnit timeUnit) throws Interrup bootstrapLatch = new CountDownLatch(1); } } + + /** + * Wait for Invalid consistency check. + * + * @return true if the client state is inconsistent + * @throws TimeoutException if bootstrap timeout + */ + public boolean waitForInconsistenState(long timeout, TimeUnit timeUnit) + throws InterruptedException, TimeoutException { + try { + if (bootstrapLatch.await(timeout, timeUnit)) { + if (bootstrapInvalidState.get()) + return true; + if (bootstrapFailed.get() || bootstrapSucceed.get()) + return false; + throw new TimeoutException("client bootstrap timeout"); + } + throw new TimeoutException("client bootstrap latch timeout"); + } finally { + bootstrapLatch = new CountDownLatch(1); + } + } }