From 3d8011e9903d4e40c8f398b4f49124170ac05ab8 Mon Sep 17 00:00:00 2001 From: Simon Bernard Date: Tue, 15 Jun 2021 16:10:47 +0200 Subject: [PATCH 01/10] client: fix NPE on ObjectEnabler.getLwM2mClient() --- .../java/org/eclipse/leshan/client/resource/ObjectEnabler.java | 1 + 1 file changed, 1 insertion(+) diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/ObjectEnabler.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/ObjectEnabler.java index 78a444cd62..69ed4e2bca 100644 --- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/ObjectEnabler.java +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/ObjectEnabler.java @@ -410,6 +410,7 @@ public ContentFormat getDefaultEncodingFormat(DownlinkRequest request) { @Override public void setLwM2mClient(LwM2mClient client) { + super.setLwM2mClient(client); for (LwM2mInstanceEnabler instanceEnabler : instances.values()) { instanceEnabler.setLwM2mClient(client); } From d9ae7bb8df91bf0279f418a868712ac130a7c3b7 Mon Sep 17 00:00:00 2001 From: Simon Bernard Date: Tue, 15 Jun 2021 16:13:00 +0200 Subject: [PATCH 02/10] Implement "Bootstrap-Request Trigger" resource (1/?/9) at client side. --- .../client/californium/LeshanClient.java | 5 +++ .../eclipse/leshan/client/LwM2mClient.java | 9 ++++ .../engine/DefaultRegistrationEngine.java | 43 +++++++++++++++++++ .../client/engine/RegistrationEngine.java | 9 ++++ .../eclipse/leshan/client/object/Server.java | 7 +++ 5 files changed, 73 insertions(+) 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 3682a5fd5d..43d7d5ba1f 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 @@ -304,6 +304,11 @@ public void triggerRegistrationUpdate(ServerIdentity server) { engine.triggerRegistrationUpdate(server); } + @Override + public boolean triggerClientInitiatedBootstrap(boolean deregister) { + return engine.triggerClientInitiatedBootstrap(deregister); + } + @Override public SendResponse sendData(ServerIdentity server, ContentFormat format, List paths, long timeoutInMs) throws InterruptedException { diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/LwM2mClient.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/LwM2mClient.java index 8f2f3442d0..2c782e394f 100644 --- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/LwM2mClient.java +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/LwM2mClient.java @@ -67,6 +67,15 @@ public interface LwM2mClient { */ void triggerRegistrationUpdate(ServerIdentity server); + /** + * Trigger a client initiated bootstrap. + * + * @param deregister True if client should deregister itself before to stop. + * + * @return true if the bootstrap can be initiated. + */ + boolean triggerClientInitiatedBootstrap(boolean deregister); + /** * Send Data synchronously to a LWM2M Server. *

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 f5e757a8e5..16e2ed2788 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 @@ -760,6 +760,49 @@ public void triggerRegistrationUpdate(ServerIdentity server, RegistrationUpdate } } + @Override + public boolean triggerClientInitiatedBootstrap(final boolean deregister) { + synchronized (this) { + if (started) { + + // check if we have a bootstrap server + ServerInfo bootstrapServerInfo = ServersInfoExtractor.getBootstrapServerInfo(objectEnablers); + if (bootstrapServerInfo == null) { + return false; + } + + // stop every + cancelUpdateTask(true); + cancelRegistrationTask(); + // TODO we should manage the case where we stop in the middle of a bootstrap session ... + cancelBootstrapTask(); + + schedExecutor.submit(new Runnable() { + + @Override + public void run() { + try { + // deregister if needed + if (deregister) { + if (!registeredServers.isEmpty()) { + for (Entry registeredServer : registeredServers + .entrySet()) { + deregister(registeredServer.getValue(), registeredServer.getKey()); + } + } + } + } catch (InterruptedException e) { + } + + // schedule a new bootstrap. + scheduleClientInitiatedBootstrap(NOW); + } + }); + } + } + return true; + } + private void logExceptionOnSendRequest(String message, Exception e) { if (LOG.isDebugEnabled()) { LOG.warn(message, e); diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/RegistrationEngine.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/RegistrationEngine.java index 98409ed9d4..8e790bfc73 100644 --- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/RegistrationEngine.java +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/RegistrationEngine.java @@ -52,6 +52,15 @@ public interface RegistrationEngine { */ void triggerRegistrationUpdate(ServerIdentity server, RegistrationUpdate registrationUpdate); + /** + * Trigger a client initiated bootstrap. + * + * @param deregister True if client should deregister itself before to stop. + * + * @return true if the bootstrap can be initiated. + */ + boolean triggerClientInitiatedBootstrap(boolean deregister); + /** * Returns the current registration Id for this server. * diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/object/Server.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/object/Server.java index ee9a40881b..9924e6fa73 100644 --- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/object/Server.java +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/object/Server.java @@ -201,6 +201,13 @@ public ExecuteResponse execute(ServerIdentity identity, int resourceid, String p if (resourceid == 8) { getLwM2mClient().triggerRegistrationUpdate(identity); return ExecuteResponse.success(); + } else if (resourceid == 9) { + boolean success = getLwM2mClient().triggerClientInitiatedBootstrap(true); + if (success) + return ExecuteResponse.success(); + else { + return ExecuteResponse.badRequest("probably no bootstrap server configured"); + } } else { return super.execute(identity, resourceid, params); } From d393b88902ff21c0eb35da9b0317b023cc79f4f6 Mon Sep 17 00:00:00 2001 From: Simon Bernard Date: Tue, 15 Jun 2021 16:14:44 +0200 Subject: [PATCH 03/10] Add an integration test about "Bootstrap-Request Trigger" resource. --- .../integration/tests/BootstrapTest.java | 52 +++++++++++++++++++ .../util/BootstrapIntegrationTestHelper.java | 4 +- 2 files changed, 54 insertions(+), 2 deletions(-) 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 98d21ce60c..2782505173 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 @@ -35,7 +35,11 @@ import org.eclipse.leshan.core.node.LwM2mObjectInstance; import org.eclipse.leshan.core.request.BootstrapDiscoverRequest; import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.request.ExecuteRequest; import org.eclipse.leshan.core.request.ReadRequest; +import org.eclipse.leshan.core.request.exception.InvalidRequestException; +import org.eclipse.leshan.core.request.exception.RequestCanceledException; +import org.eclipse.leshan.core.response.ExecuteResponse; import org.eclipse.leshan.core.response.ReadResponse; import org.eclipse.leshan.integration.tests.util.BootstrapIntegrationTestHelper; import org.eclipse.leshan.integration.tests.util.TestObjectsInitializer; @@ -187,6 +191,54 @@ public void bootstrapWithDiscoverOnRoot() { Link.serialize(helper.lastDiscoverAnswer.getObjectLinks())); } + @Test + public void bootstrapWithDiscoverOnRootThenRebootstrap() throws InvalidRequestException, InterruptedException { + // Create DM Server without security & start it + helper.createServer(); + helper.server.start(); + + // Create and start bootstrap server + helper.createBootstrapServer(null, null, new BootstrapDiscoverRequest()); + helper.bootstrapServer.start(); + + // Create Client and check it is not already registered + helper.createClient(); + helper.assertClientNotRegisterered(); + + // Start it and wait for registration + helper.client.start(); + helper.waitForBootstrapFinishedAtClientSide(1); + helper.waitForRegistrationAtServerSide(1); + + // check the client is registered + helper.assertClientRegisterered(); + assertNotNull(helper.lastDiscoverAnswer); + assertEquals(ResponseCode.CONTENT, helper.lastDiscoverAnswer.getCode()); + assertEquals(";lwm2m=1.0,;ver=1.1,,;ver=1.1,;ver=1.1,", + Link.serialize(helper.lastDiscoverAnswer.getObjectLinks())); + + // re-bootstrap + try { + ExecuteResponse response = helper.server.send(helper.getCurrentRegistration(), + new ExecuteRequest("/1/0/9")); + assertTrue(response.isSuccess()); + } catch (RequestCanceledException e) { + // request can be cancelled if server does not received the execute response before the de-registration + // so we just ignore this error. + } + + // wait bootstrap finished + helper.waitForBootstrapFinishedAtClientSide(1); + + // check last discover response + assertNotNull(helper.lastDiscoverAnswer); + assertEquals(ResponseCode.CONTENT, helper.lastDiscoverAnswer.getCode()); + assertEquals( + ";lwm2m=1.0,;ver=1.1,,;ssid=2222,;ver=1.1,;ssid=2222,;ver=1.1,", + Link.serialize(helper.lastDiscoverAnswer.getObjectLinks())); + + } + @Test public void bootstrapWithDiscoverOnDevice() { // 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 4798c3cb61..58bd250c08 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 @@ -46,7 +46,7 @@ import org.eclipse.leshan.client.engine.DefaultRegistrationEngineFactory; import org.eclipse.leshan.client.object.Device; import org.eclipse.leshan.client.object.Security; -import org.eclipse.leshan.client.resource.DummyInstanceEnabler; +import org.eclipse.leshan.client.object.Server; import org.eclipse.leshan.client.resource.ObjectsInitializer; import org.eclipse.leshan.core.LwM2mId; import org.eclipse.leshan.core.SecurityMode; @@ -258,7 +258,7 @@ public void createClient(Security security, ObjectsInitializer initializer, initializer.setInstancesForObject(LwM2mId.SECURITY, security); initializer.setInstancesForObject(LwM2mId.DEVICE, new Device("Eclipse Leshan", IntegrationTestHelper.MODEL_NUMBER, "12345")); - initializer.setClassForObject(LwM2mId.SERVER, DummyInstanceEnabler.class); + initializer.setClassForObject(LwM2mId.SERVER, Server.class); createClient(initializer, additionalAttributes, preferredContentFormat, supportedContentFormat); } From e0919c3a8b6ee6a611a2451fb78588fd12b8a5f4 Mon Sep 17 00:00:00 2001 From: Simon Bernard Date: Tue, 15 Jun 2021 16:38:27 +0200 Subject: [PATCH 04/10] Add support of "uri" attribute on bootstrap discover --- .../client/servers/ServersInfoExtractor.java | 12 ++++++++ .../leshan/client/util/LinkFormatHelper.java | 18 +++++++---- .../client/util/LinkFormatHelperTest.java | 30 +++++++++++-------- .../integration/tests/BootstrapTest.java | 17 ++++++++--- 4 files changed, 56 insertions(+), 21 deletions(-) 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 7e38f731dc..8b109b6a05 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 @@ -208,6 +208,18 @@ public static Long getServerId(LwM2mObjectEnabler objectEnabler, int instanceId) } } + public static String getServerURI(LwM2mObjectEnabler objectEnabler, int instanceId) { + ReadResponse response = null; + if (objectEnabler.getId() == SECURITY) { + response = objectEnabler.read(ServerIdentity.SYSTEM, new ReadRequest(SECURITY, instanceId, SEC_SERVER_URI)); + } + if (response != null && response.isSuccess()) { + return (String) ((LwM2mResource) response.getContent()).getValue(); + } else { + return null; + } + } + public static SecurityMode getSecurityMode(LwM2mObjectInstance securityInstance) { return SecurityMode.fromCode((long) securityInstance.getResource(SEC_SECURITY_MODE).getValue()); } diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/util/LinkFormatHelper.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/util/LinkFormatHelper.java index 4c67b12601..d8c1d51b52 100644 --- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/util/LinkFormatHelper.java +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/util/LinkFormatHelper.java @@ -170,21 +170,29 @@ private static List getBootstrapObjectDescriptionWithoutRoot(LwM2mObjectEn // add instance link for (Integer instanceId : objectEnabler.getAvailableInstanceIds()) { String instanceURL = getPath("/", Integer.toString(objectEnabler.getId()), Integer.toString(instanceId)); + Map objectAttributes = new HashMap<>(); // get short id - Long shortServerId = null; if (objectEnabler.getId() == LwM2mId.SECURITY || objectEnabler.getId() == LwM2mId.SERVER) { Boolean isBootstrapServer = objectEnabler.getId() == LwM2mId.SECURITY && ServersInfoExtractor.isBootstrapServer(objectEnabler, instanceId); if (isBootstrapServer != null && !isBootstrapServer) { - shortServerId = ServersInfoExtractor.getServerId(objectEnabler, instanceId); + Long shortServerId = ServersInfoExtractor.getServerId(objectEnabler, instanceId); + if (shortServerId != null) + objectAttributes.put("ssid", shortServerId.toString()); } + + } + + // get uri + if (objectEnabler.getId() == LwM2mId.SECURITY) { + String uri = ServersInfoExtractor.getServerURI(objectEnabler, instanceId); + if (uri != null) + objectAttributes.put("uri", "\"" + uri + "\""); } // create link - if (shortServerId != null) { - Map objectAttributes = new HashMap<>(); - objectAttributes.put("ssid", shortServerId.toString()); + if (!objectAttributes.isEmpty()) { links.add(new Link(instanceURL, objectAttributes)); } else { links.add(new Link(instanceURL)); diff --git a/leshan-client-core/src/test/java/org/eclipse/leshan/client/util/LinkFormatHelperTest.java b/leshan-client-core/src/test/java/org/eclipse/leshan/client/util/LinkFormatHelperTest.java index d540a30bc9..f343bf23f3 100644 --- a/leshan-client-core/src/test/java/org/eclipse/leshan/client/util/LinkFormatHelperTest.java +++ b/leshan-client-core/src/test/java/org/eclipse/leshan/client/util/LinkFormatHelperTest.java @@ -241,17 +241,21 @@ public void encode_bootstrap_server_object_with_version() { @Test public void encode_bootstrap_security_object() { Map instancesMap = new HashMap<>(); - instancesMap.put(0, Security.noSec("coap://localhost", 111)); - instancesMap.put(1, Security.noSecBootstap("coap://localhost")); - instancesMap.put(2, Security.noSec("coap://localhost", 222)); - instancesMap.put(3, Security.noSec("coap://localhost", 333)); + instancesMap.put(0, Security.noSec("coap://localhost:11", 111)); + instancesMap.put(1, Security.noSecBootstap("coap://localhost:1")); + instancesMap.put(2, Security.noSec("coap://localhost:22", 222)); + instancesMap.put(3, Security.noSec("coap://localhost:33", 333)); ObjectEnabler objectEnabler = new ObjectEnabler(0, getObjectModel(0), instancesMap, null, ContentFormat.DEFAULT); Link[] links = LinkFormatHelper.getBootstrapObjectDescription(objectEnabler); String strLinks = Link.serialize(links); - assertEquals(";lwm2m=1.0,;ssid=111,,;ssid=222,;ssid=333", strLinks); + assertEquals(";lwm2m=1.0,;ssid=111;uri=\"coap://localhost:11\"," // + + ";uri=\"coap://localhost:1\"," // + + ";ssid=222;uri=\"coap://localhost:22\"," // + + ";ssid=333;uri=\"coap://localhost:33\"" // + , strLinks); } @@ -261,10 +265,10 @@ public void encode_bootstrap_root() { // object 0 Map securityInstances = new HashMap<>(); - securityInstances.put(0, Security.noSec("coap://localhost", 111)); - securityInstances.put(1, Security.noSecBootstap("coap://localhost")); - securityInstances.put(2, Security.noSec("coap://localhost", 222)); - securityInstances.put(3, Security.noSec("coap://localhost", 333)); + securityInstances.put(0, Security.noSec("coap://localhost:11", 111)); + securityInstances.put(1, Security.noSecBootstap("coap://localhost:1")); + securityInstances.put(2, Security.noSec("coap://localhost:22", 222)); + securityInstances.put(3, Security.noSec("coap://localhost:33", 333)); ObjectEnabler securityObjectEnabler = new ObjectEnabler(0, getObjectModel(0), securityInstances, null, ContentFormat.DEFAULT); objectEnablers.add(securityObjectEnabler); @@ -291,9 +295,11 @@ public void encode_bootstrap_root() { Link[] links = LinkFormatHelper.getBootstrapClientDescription(objectEnablers); String strLinks = Link.serialize(links); - assertEquals( - ";lwm2m=1.0,;ssid=111,,;ssid=222,;ssid=333,;ver=2.0,;ssid=333,;ver=2.0,", - strLinks); + assertEquals(";lwm2m=1.0,;ssid=111;uri=\"coap://localhost:11\"," // + + ";uri=\"coap://localhost:1\"," // + + ";ssid=222;uri=\"coap://localhost:22\"," // + + ";ssid=333;uri=\"coap://localhost:33\"," // + + ";ver=2.0,;ssid=333,;ver=2.0,", strLinks); } private ObjectModel getObjectModel(int id) { 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 2782505173..0babe7eec1 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 @@ -187,7 +187,10 @@ public void bootstrapWithDiscoverOnRoot() { helper.assertClientRegisterered(); assertNotNull(helper.lastDiscoverAnswer); assertEquals(ResponseCode.CONTENT, helper.lastDiscoverAnswer.getCode()); - assertEquals(";lwm2m=1.0,;ver=1.1,,;ver=1.1,;ver=1.1,", + assertEquals( + String.format(";lwm2m=1.0,;ver=1.1,;uri=\"coap://%s:%d\",;ver=1.1,;ver=1.1,", + helper.bootstrapServer.getUnsecuredAddress().getHostString(), + helper.bootstrapServer.getUnsecuredAddress().getPort()), Link.serialize(helper.lastDiscoverAnswer.getObjectLinks())); } @@ -214,7 +217,10 @@ public void bootstrapWithDiscoverOnRootThenRebootstrap() throws InvalidRequestEx helper.assertClientRegisterered(); assertNotNull(helper.lastDiscoverAnswer); assertEquals(ResponseCode.CONTENT, helper.lastDiscoverAnswer.getCode()); - assertEquals(";lwm2m=1.0,;ver=1.1,,;ver=1.1,;ver=1.1,", + assertEquals( + String.format(";lwm2m=1.0,;ver=1.1,;uri=\"coap://%s:%d\",;ver=1.1,;ver=1.1,", + helper.bootstrapServer.getUnsecuredAddress().getHostString(), + helper.bootstrapServer.getUnsecuredAddress().getPort()), Link.serialize(helper.lastDiscoverAnswer.getObjectLinks())); // re-bootstrap @@ -233,8 +239,11 @@ public void bootstrapWithDiscoverOnRootThenRebootstrap() throws InvalidRequestEx // check last discover response assertNotNull(helper.lastDiscoverAnswer); assertEquals(ResponseCode.CONTENT, helper.lastDiscoverAnswer.getCode()); - assertEquals( - ";lwm2m=1.0,;ver=1.1,,;ssid=2222,;ver=1.1,;ssid=2222,;ver=1.1,", + assertEquals(String.format( + ";lwm2m=1.0,;ver=1.1,;uri=\"coap://%s:%d\",;ssid=2222;uri=\"coap://%s:%d\",;ver=1.1,;ssid=2222,;ver=1.1,", + helper.bootstrapServer.getUnsecuredAddress().getHostString(), + helper.bootstrapServer.getUnsecuredAddress().getPort(), + helper.server.getUnsecuredAddress().getHostString(), helper.server.getUnsecuredAddress().getPort()), Link.serialize(helper.lastDiscoverAnswer.getObjectLinks())); } From 0d24d6d9044beed86085bba6558797960876d630 Mon Sep 17 00:00:00 2001 From: Simon Bernard Date: Wed, 16 Jun 2021 17:02:28 +0200 Subject: [PATCH 05/10] #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); + } + } } From b181ee9a7d0d1e8290a49fe8846770c489e3a33e Mon Sep 17 00:00:00 2001 From: Simon Bernard Date: Wed, 16 Jun 2021 11:40:22 +0200 Subject: [PATCH 06/10] Add autoIdForSecurityObject mode to bootstrap config. This aims to resolve issue raised at https://github.com/OpenMobileAlliance/OMA_LwM2M_for_Developers/issues/522 --- .../integration/tests/BootstrapTest.java | 28 ++++++ .../util/BootstrapIntegrationTestHelper.java | 18 +++- .../server/bootstrap/BootstrapConfig.java | 15 ++++ .../BootstrapConfigStoreTaskProvider.java | 86 ++++++++++++++++--- .../server/bootstrap/BootstrapUtil.java | 31 +++++++ 5 files changed, 163 insertions(+), 15 deletions(-) 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 561c09eef4..96d253a06e 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 @@ -299,6 +299,34 @@ public void bootstrap_create_2_bsserver() { helper.waitForInconsistentStateAtClientSide(1); } + @Test + public void bootstrap_with_auto_id_for_security_object() { + // Create DM Server without security & start it + helper.createServer(); + helper.server.start(); + + // Create and start bootstrap server + helper.createBootstrapServer(null, helper.unsecuredBootstrapWithAutoID()); + 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 succeed + helper.waitForBootstrapFinishedAtClientSide(1); + helper.waitForRegistrationAtServerSide(1); + helper.assertClientRegisterered(); + } + @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 53b1d8cc56..2dc9fca0b0 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 @@ -189,7 +189,7 @@ public Tasks getTasks(BootstrapSession session, List previousResp return tasks; } else { lastDiscoverAnswer = (BootstrapDiscoverResponse) previousResponses.get(0); - return super.getTasks(session, previousResponses); + return super.getTasks(session, null); } } }; @@ -355,10 +355,18 @@ public Iterator getAllByEndpoint(String endpoint) { } public BootstrapConfigStore unsecuredBootstrapStore() { - return unsecuredBootstrapStoreWithBsSecurityInstanceIdAt(0); + return unsecuredBootstrapStore(0, false); } - public BootstrapConfigStore unsecuredBootstrapStoreWithBsSecurityInstanceIdAt(final int instanceId) { + public BootstrapConfigStore unsecuredBootstrapStoreWithBsSecurityInstanceIdAt(Integer instanceId) { + return unsecuredBootstrapStore(instanceId, false); + } + + public BootstrapConfigStore unsecuredBootstrapWithAutoID() { + return unsecuredBootstrapStore(0, true); + } + + public BootstrapConfigStore unsecuredBootstrapStore(final Integer bsInstanceId, final boolean autoId) { return new BootstrapConfigStore() { @Override @@ -366,13 +374,15 @@ public BootstrapConfig get(String endpoint, Identity deviceIdentity, BootstrapSe BootstrapConfig bsConfig = new BootstrapConfig(); + bsConfig.autoIdForSecurityObject = autoId; + // security for BS server ServerSecurity bsSecurity = new ServerSecurity(); bsSecurity.bootstrapServer = true; bsSecurity.uri = "coap://" + bootstrapServer.getUnsecuredAddress().getHostString() + ":" + bootstrapServer.getUnsecuredAddress().getPort(); bsSecurity.securityMode = SecurityMode.NO_SEC; - bsConfig.security.put(instanceId, bsSecurity); + bsConfig.security.put(bsInstanceId, bsSecurity); // security for DM server ServerSecurity dmSecurity = new ServerSecurity(); diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfig.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfig.java index 81d35792c1..ec134ee092 100644 --- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfig.java +++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfig.java @@ -25,6 +25,7 @@ import org.eclipse.leshan.core.CertificateUsage; import org.eclipse.leshan.core.SecurityMode; import org.eclipse.leshan.core.request.BindingMode; +import org.eclipse.leshan.core.request.BootstrapDiscoverRequest; /** * A client configuration to apply to a device during a bootstrap session. @@ -43,6 +44,20 @@ */ public class BootstrapConfig { + /** + * If activated, bootstrap server will start the bootstrap session with a {@link BootstrapDiscoverRequest} to know + * what is the Security(object 0) instance ID of bootstrap server on the device, then it will use this information + * to automatically attribute IDs to {@link #security} instance. (meaning that ID defined in {@link #security} will + * not be used) + *

+ * This can be useful because bootstrap server can not be deleted and only 1 Bootstrap server can be present in + * Security Object.
+ * See OMA_LwM2M_for_Developers#522for + * more details . + */ + public boolean autoIdForSecurityObject = false; + /** * List of LWM2M path to delete. */ diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfigStoreTaskProvider.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfigStoreTaskProvider.java index 608bef4d8d..3dbada0729 100644 --- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfigStoreTaskProvider.java +++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfigStoreTaskProvider.java @@ -15,10 +15,17 @@ *******************************************************************************/ package org.eclipse.leshan.server.bootstrap; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import org.eclipse.leshan.core.Link; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.request.BootstrapDiscoverRequest; +import org.eclipse.leshan.core.response.BootstrapDiscoverResponse; import org.eclipse.leshan.core.response.LwM2mResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * An implementation of {@link BootstrapTaskProvider} which use a {@link BootstrapConfigStore} to know which requests to @@ -26,6 +33,8 @@ */ public class BootstrapConfigStoreTaskProvider implements BootstrapTaskProvider { + private static final Logger LOG = LoggerFactory.getLogger(BootstrapConfigStoreTaskProvider.class); + private BootstrapConfigStore store; public BootstrapConfigStoreTaskProvider(BootstrapConfigStore store) { @@ -39,18 +48,73 @@ public Tasks getTasks(BootstrapSession session, List previousResp if (config == null) return null; - Tasks tasks = new Tasks(); - // create requests from config - tasks.requestsToSend = BootstrapUtil.toRequests(config, session.getContentFormat()); + if (previousResponse == null && shouldStartWithDiscover(config)) { + Tasks tasks = new Tasks(); + tasks.requestsToSend = new ArrayList<>(1); + tasks.requestsToSend.add(new BootstrapDiscoverRequest()); + tasks.last = false; + return tasks; + } else { + Tasks tasks = new Tasks(); + + // handle bootstrap discover response + if (previousResponse != null) { + BootstrapDiscoverResponse response = (BootstrapDiscoverResponse) previousResponse.get(0); + if (!response.isSuccess()) { + LOG.warn( + "Bootstrap Discover return error {} : unable to continue bootstrap session with autoIdForSecurityObject mode. {}", + response, session); + return null; + } + + Integer bootstrapServerInstanceId = findBootstrapServerInstanceId(response.getObjectLinks()); + if (bootstrapServerInstanceId == null) { + LOG.warn( + "Unable to find bootstrap server instance in Security Object (0) in response {}: unable to continue bootstrap session with autoIdForSecurityObject mode. {}", + response, session); + return null; + } + + // create requests from config + tasks.requestsToSend = BootstrapUtil.toRequests(config, session.getContentFormat(), + bootstrapServerInstanceId); + } else { + // create requests from config + tasks.requestsToSend = BootstrapUtil.toRequests(config, session.getContentFormat()); + + } - // We add model for Security(0), Server(0) and ACL(2) which are the only one supported by BootstrapConfig - // We use default 1.0 model as currently BootstrapConfig support only this model version (which should be - // compatible with models of LWM2M v1.1 but without new resources) - tasks.supportedObjects = new HashMap<>(); - tasks.supportedObjects.put(0, "1.0"); - tasks.supportedObjects.put(1, "1.0"); - tasks.supportedObjects.put(2, "1.0"); + // We add model for Security(0), Server(0) and ACL(2) which are the only one supported by BootstrapConfig + // We use default 1.0 model as currently BootstrapConfig support only this model version (which should be + // compatible with models of LWM2M v1.1 but without new resources) + tasks.supportedObjects = new HashMap<>(); + tasks.supportedObjects.put(0, "1.0"); + tasks.supportedObjects.put(1, "1.0"); + tasks.supportedObjects.put(2, "1.0"); + + return tasks; + } + } + + protected boolean shouldStartWithDiscover(BootstrapConfig config) { + return config.autoIdForSecurityObject; + } - return tasks; + protected Integer findBootstrapServerInstanceId(Link[] objectLinks) { + for (Link link : objectLinks) { + if (link.getUrl().startsWith("/0/")) { + try { + LwM2mPath path = new LwM2mPath(link.getUrl()); + if (path.isObjectInstance()) { + if (!link.getAttributes().containsKey("ssid")) + return path.getObjectInstanceId(); + } + } catch (Exception e) { + // ignore if this is not a LWM2M path + LOG.warn("Invalid LwM2MPath starting by \"/0/\""); + } + } + } + return null; } } diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapUtil.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapUtil.java index 85649f9b55..f1c3a5f696 100644 --- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapUtil.java +++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapUtil.java @@ -19,6 +19,7 @@ import java.util.Collection; import java.util.List; import java.util.Map.Entry; +import java.util.TreeMap; import org.eclipse.leshan.core.LwM2mId; import org.eclipse.leshan.core.node.LwM2mMultipleResource; @@ -149,4 +150,34 @@ public static List> toRequests } return (requests); } + + public static List> toRequests(BootstrapConfig bootstrapConfig, + ContentFormat contentFormat, int bootstrapServerID) { + List> requests = new ArrayList<>(); + // handle delete + for (String path : bootstrapConfig.toDelete) { + requests.add(new BootstrapDeleteRequest(path)); + } + // handle security + int id = 0; + for (ServerSecurity security : new TreeMap<>(bootstrapConfig.security).values()) { + if (security.bootstrapServer) { + requests.add(toWriteRequest(bootstrapServerID, security, contentFormat)); + } else { + if (id == bootstrapServerID) + id++; + requests.add(toWriteRequest(id, security, contentFormat)); + id++; + } + } + // handle server + for (Entry server : bootstrapConfig.servers.entrySet()) { + requests.add(toWriteRequest(server.getKey(), server.getValue(), contentFormat)); + } + // handle acl + for (Entry acl : bootstrapConfig.acls.entrySet()) { + requests.add(toWriteRequest(acl.getKey(), acl.getValue(), contentFormat)); + } + return (requests); + } } \ No newline at end of file From 540aeec3daf65e58d4fb180456fbc3957a8b695f Mon Sep 17 00:00:00 2001 From: Simon Bernard Date: Thu, 17 Jun 2021 11:07:51 +0200 Subject: [PATCH 07/10] Add BoostrapSessionListener at bootstrap server side. --- .../integration/tests/BootstrapTest.java | 7 +- .../util/BootstrapIntegrationTestHelper.java | 41 +++++- .../util/SynchronousBootstrapListener.java | 120 ++++++++++++++++++ .../bootstrap/LeshanBootstrapServer.java | 14 +- .../LeshanBootstrapServerBuilder.java | 5 +- .../bootstrap/LeshanBootstrapServerTest.java | 6 +- .../bootstrap/BootstrapHandlerFactory.java | 4 +- .../bootstrap/BootstrapSessionAdapter.java | 67 ++++++++++ .../bootstrap/BootstrapSessionDispatcher.java | 110 ++++++++++++++++ .../bootstrap/BootstrapSessionListener.java | 91 +++++++++++++ .../bootstrap/DefaultBootstrapHandler.java | 23 +++- .../bootstrap/BootstrapHandlerTest.java | 11 +- 12 files changed, 479 insertions(+), 20 deletions(-) create mode 100644 leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/SynchronousBootstrapListener.java create mode 100644 leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionAdapter.java create mode 100644 leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionDispatcher.java create mode 100644 leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionListener.java 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 96d253a06e..cbbfa6ce8c 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 @@ -43,6 +43,7 @@ import org.eclipse.leshan.core.response.ReadResponse; import org.eclipse.leshan.integration.tests.util.BootstrapIntegrationTestHelper; import org.eclipse.leshan.integration.tests.util.TestObjectsInitializer; +import org.eclipse.leshan.server.bootstrap.BootstrapFailureCause; import org.eclipse.leshan.server.security.NonUniqueSecurityInfoException; import org.eclipse.leshan.server.security.SecurityInfo; import org.junit.After; @@ -162,7 +163,8 @@ public void bootstrapWithAdditionalAttributes() { helper.assertClientRegisterered(); // assert session contains additional attributes - assertEquals(additionalAttributes, helper.lastBootstrapSession.getBootstrapRequest().getAdditionalAttributes()); + assertEquals(additionalAttributes, + helper.getLastBootstrapSession().getBootstrapRequest().getAdditionalAttributes()); } @Test @@ -297,6 +299,8 @@ public void bootstrap_create_2_bsserver() { // ensure bootstrap session failed because of invalid state helper.waitForInconsistentStateAtClientSide(1); + helper.waitForBootstrapFailureAtServerSide(1); + assertEquals(BootstrapFailureCause.FINISH_FAILED, helper.getLastCauseOfBootstrapFailure()); } @Test @@ -323,6 +327,7 @@ public void bootstrap_with_auto_id_for_security_object() { // ensure bootstrap session succeed helper.waitForBootstrapFinishedAtClientSide(1); + helper.waitForBootstrapSuccessAtServerSide(1); helper.waitForRegistrationAtServerSide(1); helper.assertClientRegisterered(); } 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 2dc9fca0b0..d3e3ea3efa 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 @@ -40,6 +40,8 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import org.eclipse.californium.core.coap.Request; import org.eclipse.leshan.client.californium.LeshanClientBuilder; @@ -65,6 +67,7 @@ import org.eclipse.leshan.server.bootstrap.BootstrapConfig.ServerSecurity; import org.eclipse.leshan.server.bootstrap.BootstrapConfigStore; import org.eclipse.leshan.server.bootstrap.BootstrapConfigStoreTaskProvider; +import org.eclipse.leshan.server.bootstrap.BootstrapFailureCause; import org.eclipse.leshan.server.bootstrap.BootstrapSession; import org.eclipse.leshan.server.bootstrap.BootstrapTaskProvider; import org.eclipse.leshan.server.bootstrap.DefaultBootstrapSession; @@ -86,9 +89,10 @@ public class BootstrapIntegrationTestHelper extends SecureIntegrationTestHelper public LeshanBootstrapServer bootstrapServer; public final PublicKey bootstrapServerPublicKey; public final PrivateKey bootstrapServerPrivateKey; - public volatile DefaultBootstrapSession lastBootstrapSession; public volatile BootstrapDiscoverResponse lastDiscoverAnswer; + private SynchronousBootstrapListener bootstrapListener = new SynchronousBootstrapListener(); + private class TestBootstrapSessionManager extends DefaultBootstrapSessionManager { public TestBootstrapSessionManager(BootstrapSecurityStore bsSecurityStore, @@ -106,11 +110,6 @@ public BootstrapSession begin(BootstrapRequest request, Identity clientIdentity) assertThat(request.getCoapRequest(), instanceOf(Request.class)); return super.begin(request, clientIdentity); } - - @Override - public void end(BootstrapSession bsSession) { - lastBootstrapSession = (DefaultBootstrapSession) bsSession; - } } public BootstrapIntegrationTestHelper() { @@ -166,6 +165,7 @@ public void createBootstrapServer(BootstrapSecurityStore securityStore, Bootstra builder.setSecurityStore(securityStore); builder.setSessionManager(new TestBootstrapSessionManager(securityStore, bootstrapStore)); bootstrapServer = builder.build(); + setupBootstrapServerMonitoring(); } public void createBootstrapServer(BootstrapSecurityStore securityStore, BootstrapConfigStore bootstrapStore, @@ -196,6 +196,7 @@ public Tasks getTasks(BootstrapSession session, List previousResp builder.setSecurityStore(securityStore); builder.setSessionManager(new TestBootstrapSessionManager(securityStore, taskProvider)); bootstrapServer = builder.build(); + setupBootstrapServerMonitoring(); } public void createBootstrapServer(BootstrapSecurityStore securityStore) { @@ -216,6 +217,10 @@ public Security withoutSecurityAndInstanceId(Integer id) { return sec; } + protected void setupBootstrapServerMonitoring() { + bootstrapServer.addListener(bootstrapListener); + } + @Override public void createClient() { createClient(withoutSecurity(), null); @@ -547,6 +552,30 @@ public BootstrapConfig get(String endpoint, Identity deviceIdentity, BootstrapSe }; } + public void waitForBootstrapSuccessAtServerSide(long timeInSeconds) { + try { + bootstrapListener.waitForSuccess(timeInSeconds, TimeUnit.SECONDS); + } catch (InterruptedException | TimeoutException e) { + throw new RuntimeException(e); + } + } + + public void waitForBootstrapFailureAtServerSide(long timeInSeconds) { + try { + bootstrapListener.waitForFailure(timeInSeconds, TimeUnit.SECONDS); + } catch (InterruptedException | TimeoutException e) { + throw new RuntimeException(e); + } + } + + public DefaultBootstrapSession getLastBootstrapSession() { + return (DefaultBootstrapSession) bootstrapListener.getLastSuccessfulSession(); + } + + public BootstrapFailureCause getLastCauseOfBootstrapFailure() { + return bootstrapListener.getLastCauseOfFailure(); + } + @Override public void dispose() { super.dispose(); diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/SynchronousBootstrapListener.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/SynchronousBootstrapListener.java new file mode 100644 index 0000000000..7ec0996772 --- /dev/null +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/SynchronousBootstrapListener.java @@ -0,0 +1,120 @@ +/******************************************************************************* + * 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.integration.tests.util; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.leshan.core.request.BootstrapDownlinkRequest; +import org.eclipse.leshan.core.request.BootstrapRequest; +import org.eclipse.leshan.core.request.Identity; +import org.eclipse.leshan.core.response.LwM2mResponse; +import org.eclipse.leshan.server.bootstrap.BootstrapFailureCause; +import org.eclipse.leshan.server.bootstrap.BootstrapSession; +import org.eclipse.leshan.server.bootstrap.BootstrapSessionListener; + +public class SynchronousBootstrapListener implements BootstrapSessionListener { + + private CountDownLatch finishedLatch = new CountDownLatch(1); + private CountDownLatch failedLatch = new CountDownLatch(1); + private BootstrapSession lastSucessfulSession; + private BootstrapFailureCause lastCause; + + @Override + public void sessionInitiated(BootstrapRequest request, Identity clientIdentity) { + } + + @Override + public void unAuthorized(BootstrapRequest request, Identity clientIdentity) { + } + + @Override + public void authorized(BootstrapSession session) { + + } + + @Override + public void noConfig(BootstrapSession session) { + } + + @Override + public void sendRequest(BootstrapSession session, BootstrapDownlinkRequest request) { + } + + @Override + public void onResponseSuccess(BootstrapSession session, BootstrapDownlinkRequest request, + LwM2mResponse response) { + } + + @Override + public void onResponseError(BootstrapSession session, BootstrapDownlinkRequest request, + LwM2mResponse response) { + } + + @Override + public void onRequestFailure(BootstrapSession session, BootstrapDownlinkRequest request, + Throwable cause) { + } + + @Override + public void end(BootstrapSession session) { + lastSucessfulSession = session; + finishedLatch.countDown(); + } + + @Override + public void failed(BootstrapSession session, BootstrapFailureCause cause) { + lastCause = cause; + failedLatch.countDown(); + } + + /** + * Wait until next bootstrap success event. + * + * @throws TimeoutException if wait timeouts + */ + public void waitForSuccess(long timeout, TimeUnit timeUnit) throws InterruptedException, TimeoutException { + try { + if (!finishedLatch.await(timeout, timeUnit)) + throw new TimeoutException("wait for bs success timeout"); + } finally { + finishedLatch = new CountDownLatch(1); + } + } + + /** + * Wait until next bootstrap failure event. + * + * @throws TimeoutException if wait timeouts + */ + public void waitForFailure(long timeout, TimeUnit timeUnit) throws InterruptedException, TimeoutException { + try { + if (!failedLatch.await(timeout, timeUnit)) + throw new TimeoutException("wait for bs failure timeout"); + } finally { + failedLatch = new CountDownLatch(1); + } + } + + public BootstrapSession getLastSuccessfulSession() { + return lastSucessfulSession; + } + + public BootstrapFailureCause getLastCauseOfFailure() { + return lastCause; + } +} diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/LeshanBootstrapServer.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/LeshanBootstrapServer.java index ce0e36738a..9818dc113e 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/LeshanBootstrapServer.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/LeshanBootstrapServer.java @@ -31,6 +31,8 @@ import org.eclipse.leshan.core.util.Validate; import org.eclipse.leshan.server.bootstrap.BootstrapHandler; import org.eclipse.leshan.server.bootstrap.BootstrapHandlerFactory; +import org.eclipse.leshan.server.bootstrap.BootstrapSessionDispatcher; +import org.eclipse.leshan.server.bootstrap.BootstrapSessionListener; import org.eclipse.leshan.server.bootstrap.BootstrapSessionManager; import org.eclipse.leshan.server.bootstrap.LwM2mBootstrapRequestSender; import org.eclipse.leshan.server.californium.RootResource; @@ -49,6 +51,7 @@ public class LeshanBootstrapServer { private final CoapServer coapServer; private final CoapEndpoint unsecuredEndpoint; private final CoapEndpoint securedEndpoint; + private final BootstrapSessionDispatcher dispatcher = new BootstrapSessionDispatcher(); private LwM2mBootstrapRequestSender requestSender; @@ -90,7 +93,8 @@ public LeshanBootstrapServer(CoapEndpoint unsecuredEndpoint, CoapEndpoint secure requestSender = createRequestSender(securedEndpoint, unsecuredEndpoint, encoder, decoder); // create bootstrap resource - CoapResource bsResource = createBootstrapResource(bsHandlerFactory.create(requestSender, bsSessionManager)); + CoapResource bsResource = createBootstrapResource( + bsHandlerFactory.create(requestSender, bsSessionManager, dispatcher)); coapServer.add(bsResource); } @@ -177,6 +181,14 @@ public InetSocketAddress getSecuredAddress() { } } + public void addListener(BootstrapSessionListener listener) { + dispatcher.addListener(listener); + } + + public void removeListener(BootstrapSessionListener listener) { + dispatcher.removeListener(listener); + } + /** *

* A CoAP API, generally needed when need to access to underlying CoAP protocol. diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/LeshanBootstrapServerBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/LeshanBootstrapServerBuilder.java index e937a2960f..b15075647c 100644 --- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/LeshanBootstrapServerBuilder.java +++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/LeshanBootstrapServerBuilder.java @@ -45,6 +45,7 @@ import org.eclipse.leshan.server.bootstrap.BootstrapConfigStoreTaskProvider; import org.eclipse.leshan.server.bootstrap.BootstrapHandler; import org.eclipse.leshan.server.bootstrap.BootstrapHandlerFactory; +import org.eclipse.leshan.server.bootstrap.BootstrapSessionListener; import org.eclipse.leshan.server.bootstrap.BootstrapSessionManager; import org.eclipse.leshan.server.bootstrap.DefaultBootstrapHandler; import org.eclipse.leshan.server.bootstrap.DefaultBootstrapSessionManager; @@ -401,8 +402,8 @@ public LeshanBootstrapServer build() { bootstrapHandlerFactory = new BootstrapHandlerFactory() { @Override public BootstrapHandler create(LwM2mBootstrapRequestSender sender, - BootstrapSessionManager sessionManager) { - return new DefaultBootstrapHandler(sender, sessionManager); + BootstrapSessionManager sessionManager, BootstrapSessionListener listener) { + return new DefaultBootstrapHandler(sender, sessionManager, listener); } }; if (configStore == null) { diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/bootstrap/LeshanBootstrapServerTest.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/bootstrap/LeshanBootstrapServerTest.java index b57dc438bf..fc004c6923 100644 --- a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/bootstrap/LeshanBootstrapServerTest.java +++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/bootstrap/LeshanBootstrapServerTest.java @@ -28,6 +28,7 @@ import org.eclipse.leshan.server.bootstrap.BootstrapHandler; import org.eclipse.leshan.server.bootstrap.BootstrapHandlerFactory; import org.eclipse.leshan.server.bootstrap.BootstrapSession; +import org.eclipse.leshan.server.bootstrap.BootstrapSessionListener; import org.eclipse.leshan.server.bootstrap.BootstrapSessionManager; import org.eclipse.leshan.server.bootstrap.DefaultBootstrapHandler; import org.eclipse.leshan.server.bootstrap.LwM2mBootstrapRequestSender; @@ -43,8 +44,9 @@ private LeshanBootstrapServer createBootstrapServer() { builder.setBootstrapHandlerFactory(new BootstrapHandlerFactory() { @Override - public BootstrapHandler create(LwM2mBootstrapRequestSender sender, BootstrapSessionManager sessionManager) { - bsHandler = new DefaultBootstrapHandler(sender, sessionManager); + public BootstrapHandler create(LwM2mBootstrapRequestSender sender, BootstrapSessionManager sessionManager, + BootstrapSessionListener listener) { + bsHandler = new DefaultBootstrapHandler(sender, sessionManager, listener); return bsHandler; } }); diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerFactory.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerFactory.java index da38072eaf..c65b3c1002 100644 --- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerFactory.java +++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerFactory.java @@ -27,7 +27,9 @@ public interface BootstrapHandlerFactory { * * @param sender the class responsible to send LWM2M request during a bootstapSession. * @param sessionManager the manager responsible to handle bootstrap session. + * @param listener a listener of bootstrap session life-cycle. * @return the new {@link BootstrapHandler}. */ - BootstrapHandler create(LwM2mBootstrapRequestSender sender, BootstrapSessionManager sessionManager); + BootstrapHandler create(LwM2mBootstrapRequestSender sender, BootstrapSessionManager sessionManager, + BootstrapSessionListener listener); } diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionAdapter.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionAdapter.java new file mode 100644 index 0000000000..e693006812 --- /dev/null +++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionAdapter.java @@ -0,0 +1,67 @@ +/******************************************************************************* + * 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.server.bootstrap; + +import org.eclipse.leshan.core.request.BootstrapDownlinkRequest; +import org.eclipse.leshan.core.request.BootstrapRequest; +import org.eclipse.leshan.core.request.Identity; +import org.eclipse.leshan.core.response.LwM2mResponse; + +public class BootstrapSessionAdapter implements BootstrapSessionListener { + + @Override + public void sessionInitiated(BootstrapRequest request, Identity clientIdentity) { + } + + @Override + public void unAuthorized(BootstrapRequest request, Identity clientIdentity) { + } + + @Override + public void authorized(BootstrapSession session) { + } + + @Override + public void noConfig(BootstrapSession session) { + } + + @Override + public void sendRequest(BootstrapSession session, BootstrapDownlinkRequest request) { + } + + @Override + public void onResponseSuccess(BootstrapSession session, BootstrapDownlinkRequest request, + LwM2mResponse response) { + } + + @Override + public void onResponseError(BootstrapSession session, BootstrapDownlinkRequest request, + LwM2mResponse response) { + } + + @Override + public void onRequestFailure(BootstrapSession session, BootstrapDownlinkRequest request, + Throwable cause) { + } + + @Override + public void end(BootstrapSession session) { + } + + @Override + public void failed(BootstrapSession session, BootstrapFailureCause cause) { + } +} diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionDispatcher.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionDispatcher.java new file mode 100644 index 0000000000..ee0fa05cf7 --- /dev/null +++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionDispatcher.java @@ -0,0 +1,110 @@ +/******************************************************************************* + * 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.server.bootstrap; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.eclipse.leshan.core.request.BootstrapDownlinkRequest; +import org.eclipse.leshan.core.request.BootstrapRequest; +import org.eclipse.leshan.core.request.Identity; +import org.eclipse.leshan.core.response.LwM2mResponse; + +public class BootstrapSessionDispatcher implements BootstrapSessionListener { + + private List listeners = new CopyOnWriteArrayList<>(); + + public void addListener(BootstrapSessionListener listener) { + listeners.add(listener); + } + + public void removeListener(BootstrapSessionListener listener) { + listeners.remove(listener); + } + + @Override + public void sessionInitiated(BootstrapRequest request, Identity clientIdentity) { + for (BootstrapSessionListener listener : listeners) { + listener.sessionInitiated(request, clientIdentity); + } + } + + @Override + public void unAuthorized(BootstrapRequest request, Identity clientIdentity) { + for (BootstrapSessionListener listener : listeners) { + listener.unAuthorized(request, clientIdentity); + } + } + + @Override + public void authorized(BootstrapSession session) { + for (BootstrapSessionListener listener : listeners) { + listener.authorized(session); + } + } + + @Override + public void noConfig(BootstrapSession session) { + for (BootstrapSessionListener listener : listeners) { + listener.noConfig(session); + } + } + + @Override + public void sendRequest(BootstrapSession session, BootstrapDownlinkRequest request) { + for (BootstrapSessionListener listener : listeners) { + listener.sendRequest(session, request); + } + } + + @Override + public void onResponseSuccess(BootstrapSession session, BootstrapDownlinkRequest request, + LwM2mResponse response) { + for (BootstrapSessionListener listener : listeners) { + listener.onResponseSuccess(session, request, response); + } + } + + @Override + public void onResponseError(BootstrapSession session, BootstrapDownlinkRequest request, + LwM2mResponse response) { + for (BootstrapSessionListener listener : listeners) { + listener.onResponseError(session, request, response); + } + } + + @Override + public void onRequestFailure(BootstrapSession session, BootstrapDownlinkRequest request, + Throwable cause) { + for (BootstrapSessionListener listener : listeners) { + listener.onRequestFailure(session, request, cause); + } + } + + @Override + public void end(BootstrapSession session) { + for (BootstrapSessionListener listener : listeners) { + listener.end(session); + } + } + + @Override + public void failed(BootstrapSession session, BootstrapFailureCause cause) { + for (BootstrapSessionListener listener : listeners) { + listener.failed(session, cause); + } + } +} diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionListener.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionListener.java new file mode 100644 index 0000000000..e8277a4516 --- /dev/null +++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionListener.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * 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.server.bootstrap; + +import org.eclipse.leshan.core.request.BootstrapDownlinkRequest; +import org.eclipse.leshan.core.request.BootstrapRequest; +import org.eclipse.leshan.core.request.Identity; +import org.eclipse.leshan.core.response.LwM2mResponse; + +public interface BootstrapSessionListener { + + /** + * Called when a client try to initiate a session + */ + void sessionInitiated(BootstrapRequest request, Identity clientIdentity); + + /** + * Called if client is not authorized to start a bootstrap session + */ + void unAuthorized(BootstrapRequest request, Identity clientIdentity); + + /** + * Called if client is authorized to start a bootstrap session + */ + void authorized(BootstrapSession session); + + /** + * Called if there is no configuration to apply for this client + */ + void noConfig(BootstrapSession session); + + /** + * Called when a request is sent + */ + void sendRequest(BootstrapSession session, BootstrapDownlinkRequest request); + + /** + * Called when we receive a successful response to a request. + * + * @param session the bootstrap session concerned. + * @param request The request for which we get a successful response. + * @param response The response received. + */ + void onResponseSuccess(BootstrapSession session, BootstrapDownlinkRequest request, + LwM2mResponse response); + + /** + * Called when we receive a error response to a request. + * + * @param session the bootstrap session concerned. + * @param request The request for which we get a error response. + * @param response The response received. + * + */ + public void onResponseError(BootstrapSession session, BootstrapDownlinkRequest request, + LwM2mResponse response); + + /** + * Called when a request failed to be sent. + * + * @param session the bootstrap session concerned. + * @param request The request which failed to be sent. + * @param cause The cause of the failure. Can be null. + * + */ + public void onRequestFailure(BootstrapSession session, BootstrapDownlinkRequest request, + Throwable cause); + + /** + * Called when bootstrap session finished successfully + */ + void end(BootstrapSession session); + + /** + * Called when bootstrap session failed + */ + void failed(BootstrapSession session, BootstrapFailureCause cause); +} diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapHandler.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapHandler.java index 22aef26356..402b816250 100644 --- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapHandler.java +++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapHandler.java @@ -28,6 +28,7 @@ import org.eclipse.leshan.core.response.LwM2mResponse; import org.eclipse.leshan.core.response.ResponseCallback; import org.eclipse.leshan.core.response.SendableResponse; +import org.eclipse.leshan.core.util.Validate; import org.eclipse.leshan.server.bootstrap.BootstrapSessionManager.BootstrapPolicy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,15 +56,21 @@ public class DefaultBootstrapHandler implements BootstrapHandler { protected final ConcurrentHashMap onGoingSession = new ConcurrentHashMap<>(); protected final BootstrapSessionManager sessionManager; + protected final BootstrapSessionListener listener; - public DefaultBootstrapHandler(LwM2mBootstrapRequestSender sender, BootstrapSessionManager sessionManager) { - this(sender, sessionManager, DEFAULT_TIMEOUT); + public DefaultBootstrapHandler(LwM2mBootstrapRequestSender sender, BootstrapSessionManager sessionManager, + BootstrapSessionListener listener) { + this(sender, sessionManager, listener, DEFAULT_TIMEOUT); } public DefaultBootstrapHandler(LwM2mBootstrapRequestSender sender, BootstrapSessionManager sessionManager, - long requestTimeout) { + BootstrapSessionListener listener, long requestTimeout) { + Validate.notNull(sender); + Validate.notNull(sessionManager); + Validate.notNull(listener); this.sender = sender; this.sessionManager = sessionManager; + this.listener = listener; this.requestTimeout = requestTimeout; } @@ -74,11 +81,14 @@ public SendableResponse bootstrap(Identity sender, BootstrapR // Start session, checking the BS credentials final BootstrapSession session; session = sessionManager.begin(request, sender); + listener.sessionInitiated(request, sender); if (!session.isAuthorized()) { sessionManager.failed(session, UNAUTHORIZED); + listener.unAuthorized(request, sender); return new SendableResponse<>(BootstrapResponse.badRequest("Unauthorized")); } + listener.authorized(session); // check if there is not an ongoing session. BootstrapSession oldSession = onGoingSession.put(endpoint, session); @@ -94,6 +104,7 @@ public SendableResponse bootstrap(Identity sender, BootstrapR // check if there is a configuration to apply for this device if (!sessionManager.hasConfigFor(session)) { LOG.debug("No bootstrap config for {}", session); + listener.noConfig(session); stopSession(session, NO_BOOTSTRAP_CONFIG); return new SendableResponse<>(BootstrapResponse.badRequest("no bootstrap config")); } @@ -127,8 +138,10 @@ protected void stopSession(BootstrapSession session, BootstrapFailureCause cause // if there is no cause of failure, this is a success if (cause == null) { sessionManager.end(session); + listener.end(session); } else { sessionManager.failed(session, cause); + listener.failed(session, cause); } } @@ -136,16 +149,19 @@ protected void stopSession(BootstrapSession session, BootstrapFailureCause cause protected void sendRequest(final BootstrapSession session, final BootstrapDownlinkRequest requestToSend) { + listener.sendRequest(session, requestToSend); send(session, requestToSend, new SafeResponseCallback(session) { @Override public void safeOnResponse(LwM2mResponse response) { if (response.isSuccess()) { LOG.trace("{} receives {} for {}", session, response, requestToSend); BootstrapPolicy policy = sessionManager.onResponseSuccess(session, requestToSend, response); + listener.onResponseSuccess(session, requestToSend, response); afterRequest(session, policy, requestToSend); } else { LOG.debug("{} receives {} for {}", session, response, requestToSend); BootstrapPolicy policy = sessionManager.onResponseError(session, requestToSend, response); + listener.onResponseError(session, requestToSend, response); afterRequest(session, policy, requestToSend); } } @@ -154,6 +170,7 @@ public void safeOnResponse(LwM2mResponse response) { public void safeOnError(Exception e) { LOG.debug("Error for {} while sending {} ", session, requestToSend, e); BootstrapPolicy policy = sessionManager.onRequestFailure(session, requestToSend, e); + listener.onRequestFailure(session, requestToSend, e); afterRequest(session, policy, requestToSend); } }); diff --git a/leshan-server-core/src/test/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerTest.java b/leshan-server-core/src/test/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerTest.java index 9a136d2083..92d3efd894 100644 --- a/leshan-server-core/src/test/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerTest.java +++ b/leshan-server-core/src/test/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerTest.java @@ -49,7 +49,8 @@ public void error_if_not_authorized() { // prepare bootstrapHandler with a session manager which does not authorized any session BootstrapSessionManager bsSessionManager = new MockBootstrapSessionManager(false, new InMemoryBootstrapConfigStore()); - BootstrapHandler bsHandler = new DefaultBootstrapHandler(null, bsSessionManager); + BootstrapHandler bsHandler = new DefaultBootstrapHandler(new MockRequestSender(Mode.ALWAYS_SUCCESS), + bsSessionManager, new BootstrapSessionDispatcher()); // Try to bootstrap BootstrapResponse response = bsHandler @@ -70,7 +71,8 @@ public void bootstrap_success() throws InvalidConfigurationException { bsStore.add("endpoint", new BootstrapConfig()); MockBootstrapSessionManager bsSessionManager = new MockBootstrapSessionManager(true, bsStore); - BootstrapHandler bsHandler = new DefaultBootstrapHandler(requestSender, bsSessionManager); + BootstrapHandler bsHandler = new DefaultBootstrapHandler(requestSender, bsSessionManager, + new BootstrapSessionDispatcher()); // Try to bootstrap SendableResponse sendableResponse = bsHandler @@ -91,7 +93,8 @@ public void bootstrap_failed_because_of_sent_failure() throws InvalidConfigurati EditableBootstrapConfigStore bsStore = new InMemoryBootstrapConfigStore(); bsStore.add("endpoint", new BootstrapConfig()); MockBootstrapSessionManager bsSessionManager = new MockBootstrapSessionManager(true, bsStore); - BootstrapHandler bsHandler = new DefaultBootstrapHandler(requestSender, bsSessionManager); + BootstrapHandler bsHandler = new DefaultBootstrapHandler(requestSender, bsSessionManager, + new BootstrapSessionDispatcher()); // Try to bootstrap SendableResponse sendableResponse = bsHandler @@ -115,7 +118,7 @@ public void two_bootstrap_at_the_same_time_not_allowed() bsStore.add("endpoint", new BootstrapConfig()); MockBootstrapSessionManager bsSessionManager = new MockBootstrapSessionManager(true, bsStore); BootstrapHandler bsHandler = new DefaultBootstrapHandler(requestSender, bsSessionManager, - DefaultBootstrapHandler.DEFAULT_TIMEOUT); + new BootstrapSessionDispatcher(), DefaultBootstrapHandler.DEFAULT_TIMEOUT); // First bootstrap : which will not end (because of sender) SendableResponse first_response = bsHandler From a06a0436f946710b3d788452612876b751fb48f7 Mon Sep 17 00:00:00 2001 From: Simon Bernard Date: Thu, 17 Jun 2021 16:42:52 +0200 Subject: [PATCH 08/10] Add BootstrapRequestChecker for bootstrap integration tests. --- .../integration/tests/BootstrapTest.java | 8 +++ .../tests/util/BootstrapRequestChecker.java | 61 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/BootstrapRequestChecker.java 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 cbbfa6ce8c..6bf6d5e7a2 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 @@ -42,6 +42,7 @@ import org.eclipse.leshan.core.response.ExecuteResponse; import org.eclipse.leshan.core.response.ReadResponse; import org.eclipse.leshan.integration.tests.util.BootstrapIntegrationTestHelper; +import org.eclipse.leshan.integration.tests.util.BootstrapRequestChecker; import org.eclipse.leshan.integration.tests.util.TestObjectsInitializer; import org.eclipse.leshan.server.bootstrap.BootstrapFailureCause; import org.eclipse.leshan.server.security.NonUniqueSecurityInfoException; @@ -98,6 +99,8 @@ public void bootstrap_tlv_only() { // Create and start bootstrap server helper.createBootstrapServer(null); helper.bootstrapServer.start(); + BootstrapRequestChecker contentFormatChecker = BootstrapRequestChecker.contentFormatChecker(ContentFormat.TLV); + helper.bootstrapServer.addListener(contentFormatChecker); // Create Client and check it is not already registered ContentFormat noPreferredFormat = null; // if no preferred content format server should use TLV @@ -108,6 +111,7 @@ public void bootstrap_tlv_only() { // Start it and wait for registration helper.client.start(); helper.waitForRegistrationAtServerSide(1); + assertTrue("not expected content format used", contentFormatChecker.isValid()); // check the client is registered helper.assertClientRegisterered(); @@ -122,6 +126,9 @@ public void bootstrap_senmlcbor_only() { // Create and start bootstrap server helper.createBootstrapServer(null); helper.bootstrapServer.start(); + BootstrapRequestChecker contentFormatChecker = BootstrapRequestChecker + .contentFormatChecker(ContentFormat.SENML_CBOR); + helper.bootstrapServer.addListener(contentFormatChecker); // Create Client and check it is not already registered ContentFormat preferredFormat = ContentFormat.SENML_CBOR; @@ -132,6 +139,7 @@ public void bootstrap_senmlcbor_only() { // Start it and wait for registration helper.client.start(); helper.waitForRegistrationAtServerSide(1); + assertTrue("not expected content format used", contentFormatChecker.isValid()); // check the client is registered helper.assertClientRegisterered(); diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/BootstrapRequestChecker.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/BootstrapRequestChecker.java new file mode 100644 index 0000000000..3a198ce2d3 --- /dev/null +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/BootstrapRequestChecker.java @@ -0,0 +1,61 @@ +/******************************************************************************* + * 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.integration.tests.util; + +import org.eclipse.leshan.core.request.BootstrapDownlinkRequest; +import org.eclipse.leshan.core.request.BootstrapWriteRequest; +import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.response.LwM2mResponse; +import org.eclipse.leshan.server.bootstrap.BootstrapSession; +import org.eclipse.leshan.server.bootstrap.BootstrapSessionAdapter; + +public class BootstrapRequestChecker extends BootstrapSessionAdapter { + + public interface RequestValidator { + boolean validate(BootstrapDownlinkRequest request); + } + + private RequestValidator validator; + private boolean valid = true; + + public BootstrapRequestChecker(RequestValidator validator) { + this.validator = validator; + } + + public boolean isValid() { + return valid; + } + + @Override + public void sendRequest(BootstrapSession session, BootstrapDownlinkRequest request) { + if (!validator.validate(request)) { + valid = false; + } + + } + + public static BootstrapRequestChecker contentFormatChecker(final ContentFormat expectedFormat) { + return new BootstrapRequestChecker(new RequestValidator() { + @Override + public boolean validate(BootstrapDownlinkRequest request) { + if (request instanceof BootstrapWriteRequest) { + return ((BootstrapWriteRequest) request).getContentFormat() == expectedFormat; + } + return true; + } + }); + } +} From 3d188620f394bd798f125ca9b9d7e7fa338629e1 Mon Sep 17 00:00:00 2001 From: Simon Bernard Date: Thu, 17 Jun 2021 17:02:24 +0200 Subject: [PATCH 09/10] Add a way to choose ContentFormat used for Bootstrap Write Request. --- .../integration/tests/BootstrapTest.java | 27 +++++++++++++++++++ .../util/BootstrapIntegrationTestHelper.java | 14 +++++++--- .../server/bootstrap/BootstrapConfig.java | 8 ++++++ .../BootstrapConfigStoreTaskProvider.java | 6 +++-- 4 files changed, 49 insertions(+), 6 deletions(-) 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 6bf6d5e7a2..a0e8956d9f 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 @@ -145,6 +145,33 @@ public void bootstrap_senmlcbor_only() { helper.assertClientRegisterered(); } + @Test + public void bootstrap_contentformat_from_config() { + // Create DM Server without security & start it + helper.createServer(); + helper.server.start(); + + // Create and start bootstrap server + helper.createBootstrapServer(null, helper.unsecuredBootstrap(ContentFormat.SENML_JSON)); + helper.bootstrapServer.start(); + BootstrapRequestChecker contentFormatChecker = BootstrapRequestChecker + .contentFormatChecker(ContentFormat.SENML_JSON); + helper.bootstrapServer.addListener(contentFormatChecker); + + // Create Client and check it is not already registered + ContentFormat preferredFormat = ContentFormat.SENML_CBOR; + helper.createClient(preferredFormat); + helper.assertClientNotRegisterered(); + + // Start it and wait for registration + helper.client.start(); + helper.waitForRegistrationAtServerSide(1); + assertTrue("not expected content format used", contentFormatChecker.isValid()); + + // check the client is registered + helper.assertClientRegisterered(); + } + @Test public void bootstrapWithAdditionalAttributes() { // 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 d3e3ea3efa..5eaacfb56b 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 @@ -360,18 +360,23 @@ public Iterator getAllByEndpoint(String endpoint) { } public BootstrapConfigStore unsecuredBootstrapStore() { - return unsecuredBootstrapStore(0, false); + return unsecuredBootstrapStore(0, false, null); } public BootstrapConfigStore unsecuredBootstrapStoreWithBsSecurityInstanceIdAt(Integer instanceId) { - return unsecuredBootstrapStore(instanceId, false); + return unsecuredBootstrapStore(instanceId, false, null); } public BootstrapConfigStore unsecuredBootstrapWithAutoID() { - return unsecuredBootstrapStore(0, true); + return unsecuredBootstrapStore(0, true, null); } - public BootstrapConfigStore unsecuredBootstrapStore(final Integer bsInstanceId, final boolean autoId) { + public BootstrapConfigStore unsecuredBootstrap(ContentFormat format) { + return unsecuredBootstrapStore(0, true, format); + } + + public BootstrapConfigStore unsecuredBootstrapStore(final Integer bsInstanceId, final boolean autoId, + final ContentFormat format) { return new BootstrapConfigStore() { @Override @@ -380,6 +385,7 @@ public BootstrapConfig get(String endpoint, Identity deviceIdentity, BootstrapSe BootstrapConfig bsConfig = new BootstrapConfig(); bsConfig.autoIdForSecurityObject = autoId; + bsConfig.contentFormat = format; // security for BS server ServerSecurity bsSecurity = new ServerSecurity(); diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfig.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfig.java index ec134ee092..b39735fb6d 100644 --- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfig.java +++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfig.java @@ -26,6 +26,7 @@ import org.eclipse.leshan.core.SecurityMode; import org.eclipse.leshan.core.request.BindingMode; import org.eclipse.leshan.core.request.BootstrapDiscoverRequest; +import org.eclipse.leshan.core.request.ContentFormat; /** * A client configuration to apply to a device during a bootstrap session. @@ -58,6 +59,13 @@ public class BootstrapConfig { */ public boolean autoIdForSecurityObject = false; + /** + * Content format used to send requests. + *

+ * if null content format used is the preferred one by the client (pct attribute from BootstrapRequest) + */ + public ContentFormat contentFormat = null; + /** * List of LWM2M path to delete. */ diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfigStoreTaskProvider.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfigStoreTaskProvider.java index 3dbada0729..83c25f1867 100644 --- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfigStoreTaskProvider.java +++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfigStoreTaskProvider.java @@ -76,11 +76,13 @@ public Tasks getTasks(BootstrapSession session, List previousResp } // create requests from config - tasks.requestsToSend = BootstrapUtil.toRequests(config, session.getContentFormat(), + tasks.requestsToSend = BootstrapUtil.toRequests(config, + config.contentFormat != null ? config.contentFormat : session.getContentFormat(), bootstrapServerInstanceId); } else { // create requests from config - tasks.requestsToSend = BootstrapUtil.toRequests(config, session.getContentFormat()); + tasks.requestsToSend = BootstrapUtil.toRequests(config, + config.contentFormat != null ? config.contentFormat : session.getContentFormat()); } From bd08eff87afea84e87f7403b8cedc7de5cf0d25f Mon Sep 17 00:00:00 2001 From: Simon Bernard Date: Thu, 17 Jun 2021 18:12:18 +0200 Subject: [PATCH 10/10] Add Objects resources from LWM2M v1.1 to BootstrapConfig. --- .../org/eclipse/leshan/core/MatchingType.java | 52 +++++++++++ .../server/bootstrap/BootstrapConfig.java | 93 ++++++++++++++++++- .../BootstrapConfigStoreTaskProvider.java | 6 +- .../server/bootstrap/BootstrapUtil.java | 26 +++++- 4 files changed, 167 insertions(+), 10 deletions(-) create mode 100644 leshan-core/src/main/java/org/eclipse/leshan/core/MatchingType.java diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/MatchingType.java b/leshan-core/src/main/java/org/eclipse/leshan/core/MatchingType.java new file mode 100644 index 0000000000..adef794744 --- /dev/null +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/MatchingType.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2020 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.core; + +import org.eclipse.leshan.core.util.datatype.ULong; + +/** + * The Matching Type Resource specifies how the certificate or raw public key in in the Server Public Key is presented. + * Four values are currently defined: + *

    + *
  • 0: Exact match. This is the default value and also corresponds to the functionality of LwM2M v1.0. Hence, if this + * resource is not present then the content of the Server Public Key Resource corresponds to this value. + *
  • 1: SHA-256 hash [RFC6234] + *
  • 2: SHA-384 hash [RFC6234] + *
  • 3: SHA-512 hash [RFC6234] + *
+ */ +public enum MatchingType { + EXACT_MATCH(0), SHA256(1), SHA384(2), SHA512(3); + + public final ULong code; + + private MatchingType(int code) { + this.code = ULong.valueOf(code); + } + + public static MatchingType fromCode(int code) { + return fromCode(ULong.valueOf(code)); + } + + public static MatchingType fromCode(ULong code) { + for (MatchingType sm : MatchingType.values()) { + if (sm.code.equals(code)) { + return sm; + } + } + throw new IllegalArgumentException(String.format("Unsupported MatchingType code : %s", code)); + } +} diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfig.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfig.java index b39735fb6d..bd51afc812 100644 --- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfig.java +++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfig.java @@ -23,10 +23,12 @@ import java.util.Map; import org.eclipse.leshan.core.CertificateUsage; +import org.eclipse.leshan.core.MatchingType; import org.eclipse.leshan.core.SecurityMode; import org.eclipse.leshan.core.request.BindingMode; import org.eclipse.leshan.core.request.BootstrapDiscoverRequest; import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.util.datatype.ULong; /** * A client configuration to apply to a device during a bootstrap session. @@ -126,12 +128,49 @@ public static class ServerConfig { */ public EnumSet binding = EnumSet.of(BindingMode.U); + /** + * If this resource is defined, it provides a link to the APN connection profile Object Instance (OMNA + * registered Object ID:11) to be used to communicate with this server. + *

+ * Since Server v1.1 + */ + public Integer apnLink = null; + + /** + * Using the Trigger Resource a LwM2M Client can indicate whether it is reachable over SMS (value set to 'true') + * or not (value set to 'false'). The default value (resource not present) is 'false'. When set to 'true' the + * LwM2M Server MAY, for example, request the LwM2M Client to perform operations, such as the "Update" operation + * by sending an "Execute" operation on "Registration Update Trigger" Resource via SMS. No SMS response is + * expected for such a message. + *

+ * Since Server v1.1 + */ + public Boolean trigger = null; + + /** + * Only a single transport binding SHALL be present. When the LwM2M client supports multiple transports, it MAY + * use this transport to initiate a connection. This resource can also be used to switch between multiple + * transports e.g. a non-IP device can switch to UDP transport to perform firmware updates. + *

+ * Since Server v1.1 + */ + public BindingMode preferredTransport = null; + /** + * If true or the Resource is not present, the LwM2M Client Send command capability is de-activated. If false, + * the LwM2M Client Send Command capability is activated. * + *

+ * Since Server v1.1 + */ + public Boolean muteSend = null; + @Override public String toString() { return String.format( - "ServerConfig [shortId=%s, lifetime=%s, defaultMinPeriod=%s, defaultMaxPeriod=%s, disableTimeout=%s, notifIfDisabled=%s, binding=%s]", - shortId, lifetime, defaultMinPeriod, defaultMaxPeriod, disableTimeout, notifIfDisabled, binding); + "ServerConfig [shortId=%s, lifetime=%s, defaultMinPeriod=%s, defaultMaxPeriod=%s, disableTimeout=%s, notifIfDisabled=%s, binding=%s, apnLink=%s, trigger=%s, preferredTransport=%s, muteSend=%s]", + shortId, lifetime, defaultMinPeriod, defaultMaxPeriod, disableTimeout, notifIfDisabled, binding, + apnLink, trigger, preferredTransport, muteSend); } + } /** @@ -242,6 +281,29 @@ public static class ServerSecurity { */ public Integer bootstrapServerAccountTimeout = 0; + /** + * The Matching Type Resource specifies how the certificate or raw public key in in the Server Public Key is + * presented. Four values are currently defined: + *

    + *
  • 0: Exact match. This is the default value and also corresponds to the functionality of LwM2M v1.0. Hence, + * if this resource is not present then the content of the Server Public Key Resource corresponds to this value. + *
  • 1: SHA-256 hash [RFC6234] + *
  • 2: SHA-384 hash [RFC6234] + *
  • 3: SHA-512 hash [RFC6234] + *
+ * Since Security v1.1 + */ + public MatchingType matchingType = null; + + /** + * This resource holds the value of the Server Name Indication (SNI) value to be used during the TLS handshake. + * When this resource is present then the LwM2M Server URI acts as the address of the service while the SNI + * value is used for matching a presented certificate, or PSK identity. * + *

+ * Since Security v1.1 + */ + public String sni = null; + /** * The Certificate Usage Resource specifies the semantic of the certificate or raw public key stored in the * Server Public Key Resource, which is used to match the certificate presented in the TLS/DTLS handshake. @@ -251,17 +313,38 @@ public static class ServerSecurity { *

  • 2: trust anchor assertion *
  • 3: domain-issued certificate (default if missing) * + *

    + * Since Security v1.1 + */ + public CertificateUsage certificateUsage = null; + + /** + * When this resource is present it instructs the TLS/DTLS client to propose the indicated ciphersuite(s) in the + * ClientHello of the handshake. A ciphersuite is indicated as a 32-bit integer value. The IANA TLS ciphersuite + * registry is maintained at https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml. As an + * example, the TLS_PSK_WITH_AES_128_CCM_8 ciphersuite is represented with the following string "0xC0,0xA8". To + * form an integer value the two values are concatenated. In this example, the value is 0xc0a8 or 49320. + *

    + * Since Security v1.1 + */ + public ULong cipherSuite = null; + + /** + * The Object ID of the OSCORE Object Instance that holds the OSCORE configuration to be used by the LWM2M + * Client to the LWM2M Server associated with this Security object. + * */ - public CertificateUsage certificateUsage; + public Integer oscoreSecurityMode = null; @Override public String toString() { // Note : secretKey and smsBindingKeySecret are explicitly excluded from the display for security purposes return String.format( - "ServerSecurity [uri=%s, bootstrapServer=%s, securityMode=%s, publicKeyOrId=%s, serverPublicKey=%s, smsSecurityMode=%s, smsBindingKeySecret=%s, serverSmsNumber=%s, serverId=%s, clientOldOffTime=%s, bootstrapServerAccountTimeout=%s, certificateUsage=%s]", + "ServerSecurity [uri=%s, bootstrapServer=%s, securityMode=%s, publicKeyOrId=%s, serverPublicKey=%s, smsSecurityMode=%s, smsBindingKeyParam=%s, serverSmsNumber=%s, serverId=%s, clientOldOffTime=%s, bootstrapServerAccountTimeout=%s, matchingType=%s, certificateUsage=%s, sni=%s, cipherSuite=%s, oscoreSecurityMode=%s]", uri, bootstrapServer, securityMode, Arrays.toString(publicKeyOrId), Arrays.toString(serverPublicKey), smsSecurityMode, Arrays.toString(smsBindingKeyParam), - serverSmsNumber, serverId, clientOldOffTime, bootstrapServerAccountTimeout, certificateUsage); + serverSmsNumber, serverId, clientOldOffTime, bootstrapServerAccountTimeout, matchingType, + certificateUsage, sni, cipherSuite, oscoreSecurityMode); } } diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfigStoreTaskProvider.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfigStoreTaskProvider.java index 83c25f1867..e153fd0e50 100644 --- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfigStoreTaskProvider.java +++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfigStoreTaskProvider.java @@ -87,11 +87,9 @@ public Tasks getTasks(BootstrapSession session, List previousResp } // We add model for Security(0), Server(0) and ACL(2) which are the only one supported by BootstrapConfig - // We use default 1.0 model as currently BootstrapConfig support only this model version (which should be - // compatible with models of LWM2M v1.1 but without new resources) tasks.supportedObjects = new HashMap<>(); - tasks.supportedObjects.put(0, "1.0"); - tasks.supportedObjects.put(1, "1.0"); + tasks.supportedObjects.put(0, "1.1"); + tasks.supportedObjects.put(1, "1.1"); tasks.supportedObjects.put(2, "1.0"); return tasks; diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapUtil.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapUtil.java index f1c3a5f696..eed6ec3cab 100644 --- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapUtil.java +++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapUtil.java @@ -28,6 +28,7 @@ import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.node.LwM2mSingleResource; +import org.eclipse.leshan.core.node.ObjectLink; import org.eclipse.leshan.core.request.BindingMode; import org.eclipse.leshan.core.request.BootstrapDeleteRequest; import org.eclipse.leshan.core.request.BootstrapDownlinkRequest; @@ -42,6 +43,7 @@ public class BootstrapUtil { public static LwM2mObjectInstance toSecurityInstance(int instanceId, ServerSecurity securityConfig) { Collection resources = new ArrayList<>(); + // resources since v1.0 if (securityConfig.uri != null) resources.add(LwM2mSingleResource.newStringResource(0, securityConfig.uri)); resources.add(LwM2mSingleResource.newBooleanResource(1, securityConfig.bootstrapServer)); @@ -67,9 +69,20 @@ public static LwM2mObjectInstance toSecurityInstance(int instanceId, ServerSecur resources.add(LwM2mSingleResource.newIntegerResource(11, securityConfig.clientOldOffTime)); if (securityConfig.bootstrapServerAccountTimeout != null) resources.add(LwM2mSingleResource.newIntegerResource(12, securityConfig.bootstrapServerAccountTimeout)); + + // resources since v1.1 + if (securityConfig.matchingType != null) + resources.add(LwM2mSingleResource.newUnsignedIntegerResource(13, securityConfig.matchingType.code)); + if (securityConfig.sni != null) + resources.add(LwM2mSingleResource.newStringResource(14, securityConfig.sni)); if (securityConfig.certificateUsage != null) resources.add(LwM2mSingleResource.newUnsignedIntegerResource(15, securityConfig.certificateUsage.code)); - + if (securityConfig.cipherSuite != null) + resources.add(LwM2mSingleResource.newUnsignedIntegerResource(16, securityConfig.cipherSuite)); + if (securityConfig.oscoreSecurityMode != null) { + resources.add(LwM2mSingleResource.newObjectLinkResource(17, + new ObjectLink(21, securityConfig.oscoreSecurityMode))); + } return new LwM2mObjectInstance(instanceId, resources); } @@ -83,6 +96,7 @@ public static BootstrapWriteRequest toWriteRequest(int instanceId, ServerSecurit public static LwM2mObjectInstance toServerInstance(int instanceId, ServerConfig serverConfig) { Collection resources = new ArrayList<>(); + // resources since v1.0 resources.add(LwM2mSingleResource.newIntegerResource(0, serverConfig.shortId)); resources.add(LwM2mSingleResource.newIntegerResource(1, serverConfig.lifetime)); if (serverConfig.defaultMinPeriod != null) @@ -95,6 +109,16 @@ public static LwM2mObjectInstance toServerInstance(int instanceId, ServerConfig if (serverConfig.binding != null) resources.add(LwM2mSingleResource.newStringResource(7, BindingMode.toString(serverConfig.binding))); + // resources since v1.1 + if (serverConfig.apnLink != null) + resources.add(LwM2mSingleResource.newObjectLinkResource(10, new ObjectLink(11, serverConfig.apnLink))); + if (serverConfig.trigger != null) + resources.add(LwM2mSingleResource.newBooleanResource(21, serverConfig.trigger)); + if (serverConfig.preferredTransport != null) + resources.add(LwM2mSingleResource.newStringResource(22, serverConfig.preferredTransport.toString())); + if (serverConfig.muteSend != null) + resources.add(LwM2mSingleResource.newBooleanResource(23, serverConfig.muteSend)); + return new LwM2mObjectInstance(instanceId, resources); }