Browse Source

Merge pull request #14084 from thingsboard/fix_lwm2m-boostrap-failed-for-BootstrapReadRequest-/1

pull/14221/head
Andrew Shvayka 8 months ago
committed by GitHub
parent
commit
00a0e77e7b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      application/src/main/resources/thingsboard.yml
  2. 43
      application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java
  3. 2
      application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java
  4. 174
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java
  5. 11
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java
  6. 32
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java
  7. 37
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java
  8. 29
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBS3SectionTriggerTest.java
  9. 37
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSLwm2mOnlyNoneTriggerOneSectionTest.java
  10. 39
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSNoTriggerTest.java
  11. 29
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSOnlyTriggerOneSectionTest.java
  12. 31
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSTriggerTest.java
  13. 50
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java
  14. 13
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java
  15. 4
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java
  16. 8
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java
  17. 6
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java
  18. 112
      common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/Lwm2mServerIdentifier.java
  19. 2
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java
  20. 4
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java
  21. 17
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2mDefaultBootstrapSessionManager.java
  22. 326
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java
  23. 11
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MConfigurationChecker.java
  24. 6
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java
  25. 3
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java
  26. 3
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java
  27. 27
      dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java
  28. 20
      dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java
  29. 70
      ui-ngx/src/app/core/http/device.service.ts
  30. 14
      ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.html
  31. 56
      ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts
  32. 3
      ui-ngx/src/app/modules/home/components/device/device-credentials.component.html
  33. 4
      ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts
  34. 10
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html
  35. 14
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts
  36. 3
      ui-ngx/src/assets/locale/locale.constant-en_US.json

2
application/src/main/resources/thingsboard.yml

@ -1199,7 +1199,7 @@ transport:
# Enable/disable Bootstrap Server
enabled: "${LWM2M_ENABLED_BS:true}"
# Default value in LwM2M client after start in mode Bootstrap for the object : name "LWM2M Security" field: "Short Server ID" (deviceProfile: Bootstrap.BOOTSTRAP SERVER.Short ID)
id: "${LWM2M_SERVER_ID_BS:111}"
id: "${LWM2M_SERVER_ID_BS:0}"
# LwM2M bootstrap server bind address. Bind to all interfaces by default
bind_address: "${LWM2M_BS_BIND_ADDRESS:0.0.0.0}"
# LwM2M bootstrap server bind port

43
application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java

@ -84,6 +84,7 @@ import org.thingsboard.server.transport.lwm2m.server.client.ResourceUpdateResult
import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler;
import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler;
import java.io.IOException;
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.Arrays;
@ -120,11 +121,11 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfil
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE;
import static org.thingsboard.server.transport.lwm2m.ota.AbstractOtaLwM2MIntegrationTest.CLIENT_LWM2M_SETTINGS_19;
@TestPropertySource(properties = {
"transport.lwm2m.enabled=true",
})
@Slf4j
@DaoSqlTest
@TestPropertySource(properties = {
"transport.lwm2m.enabled=true"
})
public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportIntegrationTest {
@SpyBean
@ -145,9 +146,6 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
public static final String host = "localhost";
public static final String hostBs = "localhost";
public static final Integer shortServerId = 123;
public static final Integer shortServerIdBs0 = 0;
public static final int serverId = 1;
public static final int serverIdBs = 0;
public static final String COAP = "coap://";
public static final String COAPS = "coaps://";
@ -317,7 +315,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
@After
public void after() throws Exception {
this.clientDestroy(true);
this.clientDestroy();
if (executor != null && !executor.isShutdown()) {
executor.shutdownNow();
}
@ -564,7 +562,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
public void createNewClient(Security security, Security securityBs, boolean isRpc,
String endpoint, Integer clientDtlsCidLength, boolean queueMode,
String deviceIdStr, Integer value3_0_9) throws Exception {
this.clientDestroy(false);
this.clientDestroy();
lwM2MTestClient = new LwM2MTestClient(this.executor, endpoint, resources);
try (ServerSocket socket = new ServerSocket(0)) {
@ -651,13 +649,30 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
}
private void clientDestroy(boolean isAfter) {
private void clientDestroy() {
try {
if (lwM2MTestClient != null && lwM2MTestClient.getLeshanClient() != null) {
if (isAfter) {
sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr());
awaitDeleteDevice(lwM2MTestClient.getDeviceIdStr());
boolean serverAlive = false;
for (int port = AbstractLwM2MIntegrationTest.port; port <= securityPortBs; port++) {
try (ServerSocket socket = new ServerSocket(port)) {
log.info("Port {} is free.", port);
} catch (IOException e) {
log.debug("Port {} is busy — CoAP server still active.", port);
serverAlive = true;
break;
}
}
if (serverAlive) {
try {
sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr());
awaitDeleteDevice(lwM2MTestClient.getDeviceIdStr());
} catch (Exception e) {
log.warn("Failed to cleanup LwM2M observations before destroy: {}", e.getMessage());
}
} else {
log.info("No active CoAP server found on ports 5685–5688. Skipping observe cleanup.");
}
lwM2MTestClient.destroy();
}
} catch (Exception e) {
@ -705,10 +720,10 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
return bootstrap;
}
private AbstractLwM2MBootstrapServerCredential getBootstrapServerCredentialNoSec(boolean isBootstrap) {
protected AbstractLwM2MBootstrapServerCredential getBootstrapServerCredentialNoSec(boolean isBootstrap) {
AbstractLwM2MBootstrapServerCredential bootstrapServerCredential = new NoSecLwM2MBootstrapServerCredential();
bootstrapServerCredential.setServerPublicKey("");
bootstrapServerCredential.setShortServerId(isBootstrap ? shortServerIdBs0 : shortServerId);
bootstrapServerCredential.setShortServerId(isBootstrap ? null : shortServerId);
bootstrapServerCredential.setBootstrapServerIs(isBootstrap);
bootstrapServerCredential.setHost(isBootstrap ? hostBs : host);
bootstrapServerCredential.setPort(isBootstrap ? portBs : port);

2
application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java

@ -24,8 +24,6 @@ public class Lwm2mTestHelper {
public static final int TEMPERATURE_SENSOR = 3303;
// Ids in Client
public static final int OBJECT_ID_0 = 0;
public static final int OBJECT_ID_1 = 1;
public static final int OBJECT_INSTANCE_ID_0 = 0;
public static final int OBJECT_INSTANCE_ID_1 = 1;
public static final int OBJECT_INSTANCE_ID_2 = 2;

174
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java

@ -68,6 +68,9 @@ import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandle
import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.HashMap;
@ -77,6 +80,7 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY;
@ -88,10 +92,7 @@ import static org.eclipse.leshan.core.LwM2mId.SECURITY;
import static org.eclipse.leshan.core.LwM2mId.SERVER;
import static org.eclipse.leshan.core.LwM2mId.SOFTWARE_MANAGEMENT;
import static org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder.getDefaultPathEncoder;
import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.serverId;
import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.serverIdBs;
import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.shortServerId;
import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.shortServerIdBs0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_FAILURE;
@ -141,7 +142,7 @@ public class LwM2MTestClient {
private LwM2mTemperatureSensor lwM2mTemperatureSensor12;
private String deviceIdStr;
public void init(Security security, Security securityBs, int port, boolean isRpc,
public void init(Security securityLwm2m, Security securityBs, int port, boolean isRpc,
LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler,
LwM2mClientContext clientContext, Integer cIdLength, boolean queueMode,
boolean supportFormatOnly_SenMLJSON_SenMLCBOR, Integer value3_0_9) throws InvalidDDFFileException, IOException {
@ -149,55 +150,33 @@ public class LwM2MTestClient {
this.defaultLwM2mUplinkMsgHandlerTest = defaultLwM2mUplinkMsgHandler;
this.clientContext = clientContext;
List<ObjectModel> models = ObjectLoader.loadAllDefault();
for (String resourceName : lwm2mClientResources) {
models.addAll(ObjectLoader.loadDdfFile(LwM2MTestClient.class.getClassLoader().getResourceAsStream("lwm2m/" + resourceName), resourceName));
ObjectsInitializer initializer = createFreshInitializer();
// SECURITY
if (securityLwm2m != null && securityLwm2m.getId() != null) {
forceNullSecurityId(securityLwm2m);
}
if (this.modelResources != null) {
List<ObjectModel> modelsRes = new ArrayList<>();
for (String resourceName : this.modelResources) {
modelsRes.addAll(ObjectLoader.loadDdfFile(LwM2MTestClient.class.getClassLoader().getResourceAsStream("lwm2m/" + resourceName), resourceName));
}
Set<Integer> idsToRemove = new HashSet<>();
for (ObjectModel model : modelsRes) {
idsToRemove.add(model.id);
}
models.removeIf(model -> idsToRemove.contains(model.id));
models.addAll(modelsRes);
if (securityBs!= null && securityBs.getId() != null) {
forceNullSecurityId(securityBs);
}
LwM2mModel model = new StaticModel(models);
ObjectsInitializer initializer = new ObjectsInitializer(model);
if (securityBs != null && security != null) {
// SECURITY
security.setId(serverId);
securityBs.setId(serverIdBs);
LwM2mInstanceEnabler[] instances = new LwM2mInstanceEnabler[]{securityBs, security};
initializer.setClassForObject(SECURITY, Security.class);
initializer.setInstancesForObject(SECURITY, instances);
// SERVER
Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60));
lwm2mServer.setId(serverId);
Server serverBs = new Server(shortServerIdBs0, TimeUnit.MINUTES.toSeconds(60));
serverBs.setId(serverIdBs);
instances = new LwM2mInstanceEnabler[]{serverBs, lwm2mServer};
initializer.setClassForObject(SERVER, Server.class);
initializer.setInstancesForObject(SERVER, instances);
if (securityBs != null && securityLwm2m != null) {
log.warn("Security Both: securityBs: [{}] and security Lwm2m [{}]", securityBs.getId(), securityLwm2m.getId());
initializer.setInstancesForObject(SECURITY, securityBs, securityLwm2m);
} else if (securityBs != null) {
// SECURITY
log.warn("Security BS only: securityBs: [{}] ", securityBs.getId());
initializer.setInstancesForObject(SECURITY, securityBs);
// SERVER
initializer.setClassForObject(SERVER, Server.class);
} else {
} else if (securityLwm2m != null){
// SECURITY
initializer.setInstancesForObject(SECURITY, security);
// SERVER
Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60));
lwm2mServer.setId(serverId);
initializer.setInstancesForObject(SERVER, lwm2mServer);
log.warn("Security Lwm2m only: security Lwm2m [{}]", securityLwm2m.getId());
initializer.setInstancesForObject(SECURITY, securityLwm2m);
}
// SERVER
Server serverLwm2m = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60));
initializer.setInstancesForObject(SERVER, serverLwm2m);
// DEVICE
initializer.setInstancesForObject(DEVICE, lwM2MDevice = new SimpleLwM2MDevice(executor, value3_0_9));
// OTHER t
initializer.setInstancesForObject(FIRMWARE, fwLwM2MDevice = new FwLwM2MDevice());
initializer.setInstancesForObject(SOFTWARE_MANAGEMENT, swLwM2MDevice = new SwLwM2MDevice());
initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class);
@ -444,25 +423,43 @@ public class LwM2MTestClient {
public void destroy() {
if (leshanClient != null) {
leshanClient.destroy(true);
}
if (lwM2MDevice != null) {
lwM2MDevice.destroy();
}
if (fwLwM2MDevice != null) {
fwLwM2MDevice.destroy();
}
if (swLwM2MDevice != null) {
swLwM2MDevice.destroy();
}
if (lwM2MBinaryAppDataContainer != null) {
lwM2MBinaryAppDataContainer.destroy();
try {
leshanClient.destroy(true);
} catch (Exception e) {
log.warn("Failed to destroy Leshan client", e);
} finally {
leshanClient = null;
}
}
if (lwM2MTemperatureSensor != null) {
lwM2MTemperatureSensor.destroy();
// ThingsBoard custom LwM2M objects
destroySafe(lwM2MDevice);
destroySafe(fwLwM2MDevice);
destroySafe(swLwM2MDevice);
destroySafe(lwM2MBinaryAppDataContainer);
destroySafe(lwM2MTemperatureSensor);
lwM2MDevice = null;
fwLwM2MDevice = null;
swLwM2MDevice = null;
lwM2MBinaryAppDataContainer = null;
lwM2MTemperatureSensor = null;
}
private void destroySafe(Object obj) {
if (obj == null) return;
try {
Method destroy = obj.getClass().getMethod("destroy");
destroy.invoke(obj);
} catch (NoSuchMethodException e) {
// не має destroy() — ігноруємо
} catch (Exception e) {
log.warn("Failed to destroy {}", obj.getClass().getSimpleName(), e);
}
}
public void start(boolean isStartLw) {
if (leshanClient != null) {
leshanClient.start();
@ -483,4 +480,59 @@ public class LwM2MTestClient {
LwM2mClient lwM2MClient = this.clientContext.getClientByEndpoint(endpoint);
Mockito.doAnswer(invocationOnMock -> null).when(defaultLwM2mUplinkMsgHandlerTest).initAttributes(lwM2MClient, true);
}
private ObjectsInitializer createFreshInitializer() {
List<ObjectModel> models = new ArrayList<>(ObjectLoader.loadAllDefault());
for (String resourceName : lwm2mClientResources) {
try (InputStream in = LwM2MTestClient.class.getClassLoader()
.getResourceAsStream("lwm2m/" + resourceName)) {
models.addAll(ObjectLoader.loadDdfFile(in, resourceName));
} catch (IOException | InvalidDDFFileException e) {
log.warn("Failed to load resource {}", resourceName, e);
}
}
if (this.modelResources != null) {
List<ObjectModel> modelsRes = new ArrayList<>();
for (String resourceName : this.modelResources) {
try (InputStream in = LwM2MTestClient.class.getClassLoader()
.getResourceAsStream("lwm2m/" + resourceName)) {
modelsRes.addAll(ObjectLoader.loadDdfFile(in, resourceName));
} catch (IOException | InvalidDDFFileException e) {
log.warn("Failed to load resource {}", resourceName, e);
}
}
Set<Integer> idsToRemove = modelsRes.stream()
.map(m -> m.id)
.collect(Collectors.toSet());
models.removeIf(m -> idsToRemove.contains(m.id));
models.addAll(modelsRes);
}
LwM2mModel model = new StaticModel(models);
return new ObjectsInitializer(model);
}
private void forceNullSecurityId(Security security) {
if (security == null) {
return;
}
try {
Field field = security.getClass().getDeclaredField("id");
field.setAccessible(true);
field.set(security, null);
log.info("[forceNullSecurityId] Set id=null for {}", security);
} catch (NoSuchFieldException e) {
try {
//(SecurityObjectInstance)
Field field = security.getClass().getSuperclass().getDeclaredField("id");
field.setAccessible(true);
field.set(security, null);
log.info("[forceNullSecurityId] Set id=null for {} (via superclass)", security);
} catch (Exception ex) {
log.error("[forceNullSecurityId] Field 'id' not found for {}", security.getClass(), ex);
}
} catch (Exception e) {
log.error("[forceNullSecurityId] Failed to set id=null for {}", security.getClass(), e);
}
}
}

11
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java

@ -43,12 +43,11 @@ import static org.awaitility.Awaitility.await;
import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL;
import static org.eclipse.leshan.core.LwM2mId.DEVICE;
import static org.eclipse.leshan.core.LwM2mId.FIRMWARE;
import static org.eclipse.leshan.core.LwM2mId.SECURITY;
import static org.eclipse.leshan.core.LwM2mId.SERVER;
import static org.eclipse.leshan.core.LwM2mId.SOFTWARE_MANAGEMENT;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_ID_1;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_12;
@ -144,10 +143,10 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg
});
}
});
String ver_Id_0 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(OBJECT_ID_0).version;
String ver_Id_1 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(OBJECT_ID_1).version;
objectIdVer_0 = "/" + OBJECT_ID_0 + "_" + ver_Id_0;
objectIdVer_1 = "/" + OBJECT_ID_1 + "_" + ver_Id_1;
String ver_Id_0 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(SECURITY).version;
String ver_Id_1 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(SERVER).version;
objectIdVer_0 = "/" + SECURITY + "_" + ver_Id_0;
objectIdVer_1 = "/" + SERVER + "_" + ver_Id_1;
objectIdVer_2 = (String) expectedObjectIdVers.stream().filter(path -> ((String) path).startsWith("/" + ACCESS_CONTROL)).findFirst().get();
objectIdVer_3 = (String) expectedObjectIdVers.stream().filter(PREDICATE_3).findFirst().get();
objectIdVer_19 = (String) expectedObjectIdVers.stream().filter(path -> ((String) path).startsWith("/" + BINARY_APP_DATA_CONTAINER)).findFirst().get();

32
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java

@ -21,8 +21,10 @@ import org.eclipse.leshan.core.ResponseCode;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest;
import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -53,18 +55,18 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest
try {
expectedObjectIdVers.forEach(expected -> {
try {
String actualResult = sendRPCById((String) expected);
String expectedObjectId = pathIdVerToObjectId((String) expected);
LwM2mPath expectedPath = new LwM2mPath(expectedObjectId);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String expectedObjectInstances = "LwM2mObject [id=" + expectedPath.getObjectId() + ", instances={0=LwM2mObjectInstance [id=0, resources=";
if (expectedPath.getObjectId() == 1) {
expectedObjectInstances = "LwM2mObject [id=1, instances={1=";
} else if (expectedPath.getObjectId() == 2) {
expectedObjectInstances = "LwM2mObject [id=2, instances={}]";
if (expectedPath.getObjectId() > ACCESS_CONTROL) {
String actualResult = sendRPCByIdSync((String) expected);
if (StringUtils.isNoneBlank(actualResult)) {
log.warn(" expectedPath: [{}]", expectedPath);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String expectedObjectInstances = "LwM2mObject [id=" + expectedPath.getObjectId() + ", instances={0=LwM2mObjectInstance [id=0, resources=";
assertTrue(rpcActualResult.get("value").asText().contains(expectedObjectInstances));
}
}
assertTrue(rpcActualResult.get("value").asText().contains(expectedObjectInstances));
} catch (Exception e) {
e.printStackTrace();
}
@ -83,7 +85,7 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest
public void testReadAllInstancesInClientById_Result_CONTENT_Value_IsInstances_IsResources() throws Exception {
expectedObjectIdVerInstances.forEach(expected -> {
try {
String actualResult = sendRPCById((String) expected);
String actualResult = sendRPCByIdAsync((String) expected);
String expectedObjectId = pathIdVerToObjectId((String) expected);
LwM2mPath expectedPath = new LwM2mPath(expectedObjectId);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
@ -104,7 +106,7 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest
@Test
public void testReadMultipleResourceById_Result_CONTENT_Value_IsLwM2mMultipleResource() throws Exception {
String expectedIdVer = objectInstanceIdVer_3 + "/" + RESOURCE_ID_11;
String actualResult = sendRPCById(expectedIdVer);
String actualResult = sendRPCByIdAsync(expectedIdVer);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String expected = "LwM2mMultipleResource [id=" + RESOURCE_ID_11 + ", values={";
@ -117,7 +119,7 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest
@Test
public void testReadSingleResourceById_Result_CONTENT_Value_IsLwM2mSingleResource() throws Exception {
String expectedIdVer = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14;
String actualResult = sendRPCById(expectedIdVer);
String actualResult = sendRPCByIdAsync(expectedIdVer);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText());
String expected = "LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value=";
@ -228,10 +230,14 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest
assertEquals(actualValue, expectedValue);
}
private String sendRPCById(String path) throws Exception {
private String sendRPCByIdAsync(String path) throws Exception {
String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}";
return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk());
}
private String sendRPCByIdSync(String path) throws Exception {
String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}";
return doPost("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk());
}
private String sendRPCByKey(String key) throws Exception {
String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"key\": \"" + key + "\"}}";

37
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java

@ -69,6 +69,7 @@ import java.util.concurrent.TimeUnit;
import static org.awaitility.Awaitility.await;
import static org.eclipse.leshan.client.object.Security.noSecBootstrap;
import static org.eclipse.leshan.client.object.Security.psk;
import static org.eclipse.leshan.core.LwM2mId.SERVER;
import static org.junit.Assert.assertEquals;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode.PSK;
@ -77,7 +78,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClient
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_STARTED;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_SUCCESS;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_ID_1;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9;
@DaoSqlTest
@ -95,7 +96,6 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M
protected static final String SERVER_STORE_PWD = "server_ks_password";
protected static final String SERVER_CERT_ALIAS = "server";
protected static final String SERVER_CERT_ALIAS_BS = "bootstrap";
protected static final Security SECURITY_NO_SEC_BS = noSecBootstrap(URI_BS);;
protected final X509Certificate serverX509Cert; // server certificate signed by rootCA
protected final X509Certificate serverX509CertBs; // serverBs certificate signed by rootCA
protected final PublicKey serverPublicKeyFromCert; // server public key used for RPK
@ -178,20 +178,20 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M
defaultBootstrapCredentials.setLwm2mServer(serverCredentials);
}
public void basicTestConnectionBefore(String clientEndpoint,
String awaitAlias,
LwM2MProfileBootstrapConfigType type,
Set<LwM2MClientState> expectedStatuses,
LwM2MClientState finishState) throws Exception {
public void basicTestConnectionStartBS(String clientEndpoint,
String awaitAlias,
LwM2MProfileBootstrapConfigType type,
Set<LwM2MClientState> expectedStatuses,
LwM2MClientState finishState) throws Exception {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsNoSec(type));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint));
this.basicTestConnection(null , SECURITY_NO_SEC_BS,
this.basicTestConnection(null , noSecBootstrap(URI_BS),
deviceCredentials,
clientEndpoint,
transportConfiguration,
awaitAlias,
expectedStatuses,
true,
false,
finishState,
false);
}
@ -231,12 +231,19 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M
}
public void basicTestConnectionBootstrapRequestTriggerBefore(String clientEndpoint, String awaitAlias, LwM2MProfileBootstrapConfigType type) throws Exception {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsNoSec(type));
public void basicTestConnectionBootstrapRequestTriggerBefore(String clientEndpoint, String awaitAlias, LwM2MProfileBootstrapConfigType type, int cnt) throws Exception {
List<LwM2MBootstrapServerCredential> bootstrapServerCredentialsNoSec = getBootstrapServerCredentialsNoSec(type);
for (int i = 2; i <= cnt; i++) {
AbstractLwM2MBootstrapServerCredential bsCredential = getBootstrapServerCredentialNoSec(false);
bsCredential.setHost("0.0.0." + i);
bsCredential.setShortServerId(bsCredential.getShortServerId() + i);
bootstrapServerCredentialsNoSec.add(bsCredential);
}
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, bootstrapServerCredentialsNoSec);
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint));
this.basicTestConnectionBootstrapRequestTrigger(
SECURITY_NO_SEC,
SECURITY_NO_SEC_BS,
noSecBootstrap(URI_BS),
deviceCredentials,
clientEndpoint,
transportConfiguration,
@ -275,8 +282,8 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M
});
Assert.assertTrue(lwM2MTestClient.getClientStates().containsAll(expectedStatusesLwm2m));
String executedPath = "/" + OBJECT_ID_1 + "_" + lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(OBJECT_ID_1).version
+ "/" +serverId + "/" + RESOURCE_ID_9;
String executedPath = "/" + SERVER + "_" + lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(SERVER).version
+ "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_9;
lwM2MTestClient.setClientStates(new HashSet<>());
String actualResult = sendRPCSecurityExecuteById(executedPath, deviceIdStr, endpoint);
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class);
@ -352,7 +359,7 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M
default:
throw new IllegalStateException("Unexpected value: " + mode);
}
bootstrapServerCredential.setShortServerId(isBootstrap ? shortServerIdBs0 : shortServerId);
bootstrapServerCredential.setShortServerId(isBootstrap ? null : shortServerId);
bootstrapServerCredential.setBootstrapServerIs(isBootstrap);
bootstrapServerCredential.setHost(isBootstrap ? hostBs : host);
bootstrapServerCredential.setPort(isBootstrap ? securityPortBs : securityPort);

29
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBS3SectionTriggerTest.java

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.security.sql;
import org.junit.Test;
import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.LWM2M_ONLY;
public class NoSecLwM2MIntegrationBS3SectionTriggerTest extends AbstractSecurityLwM2MIntegrationTest {
@Test
public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTrigger_3_ConnectBsSuccess_UpdateLwm2mSection_3_AndLm2m_1_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger_3" + LWM2M_ONLY.name();
String awaitAlias = "await on client state (NoSecBS Trigger Lwm2m section)";
basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, LWM2M_ONLY, 3);
}
}

37
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSLwm2mOnlyNoneTriggerOneSectionTest.java

@ -0,0 +1,37 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.security.sql;
import org.junit.Test;
import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.LWM2M_ONLY;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE;
public class NoSecLwM2MIntegrationBSLwm2mOnlyNoneTriggerOneSectionTest extends AbstractSecurityLwM2MIntegrationTest {
@Test
public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateLwm2mSectionAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + LWM2M_ONLY.name();
String awaitAlias = "await on client state (NoSecBS Trigger Lwm2m section)";
basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, LWM2M_ONLY, 1);
}
@Test
public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateNoneSectionAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + NONE.name();
String awaitAlias = "await on client state (NoSecBS Trigger None section)";
basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, NONE, 1);
}
}

39
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSNoTriggerTest.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.security.sql;
import org.junit.Test;
import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOTH;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.LWM2M_ONLY;
public class NoSecLwM2MIntegrationBSNoTriggerTest extends AbstractSecurityLwM2MIntegrationTest {
@Test
public void testWithNoSecConnectBsSuccess_UpdateTwoSectionsBootstrapAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "NoTrigger" + BOTH.name();
String awaitAlias = "await on client state (NoSecBS two section)";
basicTestConnectionStartBS(clientEndpoint, awaitAlias, BOTH, expectedStatusesRegistrationBsSuccess, ON_REGISTRATION_SUCCESS);
}
@Test
public void testWithNoSecConnectBsSuccess_UpdateLwm2mSectionAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "NoTrigger" + LWM2M_ONLY.name();
String awaitAlias = "await on client state (NoSecBS Lwm2m section)";
basicTestConnectionStartBS(clientEndpoint, awaitAlias, LWM2M_ONLY, expectedStatusesRegistrationBsSuccess, ON_REGISTRATION_SUCCESS);
}
}

29
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSOnlyTriggerOneSectionTest.java

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.security.sql;
import org.junit.Test;
import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOOTSTRAP_ONLY;
public class NoSecLwM2MIntegrationBSOnlyTriggerOneSectionTest extends AbstractSecurityLwM2MIntegrationTest {
@Test
public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateBootstrapSectionAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + BOOTSTRAP_ONLY.name();
String awaitAlias = "await on client state (NoSecBS Trigger Bootstrap section)";
basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, BOOTSTRAP_ONLY, 1);
}
}

31
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSTriggerTest.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.security.sql;
import org.junit.Test;
import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOTH;
public class NoSecLwM2MIntegrationBSTriggerTest extends AbstractSecurityLwM2MIntegrationTest {
@Test
public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateTwoSectionAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + BOTH.name();
String awaitAlias = "await on client state (NoSecBS Trigger Two section)";
basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, BOTH, 1);
}
}

50
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java

@ -19,12 +19,6 @@ import org.junit.Test;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOOTSTRAP_ONLY;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOTH;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.LWM2M_ONLY;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE;
public class NoSecLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTest {
//Lwm2m only
@ -34,48 +28,4 @@ public class NoSecLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationT
LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint));
super.basicTestConnectionObserveSingleTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, false, false);
}
// Bootstrap + Lwm2m
@Test
public void testWithNoSecConnectBsSuccess_UpdateTwoSectionsBootstrapAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + BOTH.name();
String awaitAlias = "await on client state (NoSecBS two section)";
basicTestConnectionBefore(clientEndpoint, awaitAlias, BOTH, expectedStatusesRegistrationBsSuccess, ON_REGISTRATION_SUCCESS);
}
@Test
public void testWithNoSecConnectBsSuccess_UpdateLwm2mSectionAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + LWM2M_ONLY.name();
String awaitAlias = "await on client state (NoSecBS Lwm2m section)";
basicTestConnectionBefore(clientEndpoint, awaitAlias, LWM2M_ONLY, expectedStatusesRegistrationBsSuccess, ON_REGISTRATION_SUCCESS);
}
// Bs trigger
@Test
public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateTwoSectionAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + BOTH.name();
String awaitAlias = "await on client state (NoSecBS Trigger Two section)";
basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, BOTH);
}
@Test
public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateBootstrapSectionAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + BOOTSTRAP_ONLY.name();
String awaitAlias = "await on client state (NoSecBS Trigger Bootstrap section)";
basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, BOOTSTRAP_ONLY);
}
@Test
public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateLwm2mSectionAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + LWM2M_ONLY.name();
String awaitAlias = "await on client state (NoSecBS Trigger Lwm2m section)";
basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, LWM2M_ONLY);
}
@Test
public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateNoneSectionAndLm2m_ConnectLwm2mSuccess() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + NONE.name();
String awaitAlias = "await on client state (NoSecBS Trigger None section)";
basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, NONE);
}
}

13
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java

@ -58,8 +58,7 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes
Hex.decodeHex(keyPsk.toCharArray()));
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false);
this.basicTestConnection(security,
null,
this.basicTestConnection(security, null,
deviceCredentials,
clientEndpoint,
transportConfiguration,
@ -85,8 +84,7 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITH_ONE_OBSERVE, getBootstrapServerCredentialsSecure(PSK, NONE));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false);
String awaitAlias = "await on client state (Psk_Lwm2m)";
Device lwm2mDevice = this.basicTestConnection(security,
null,
Device lwm2mDevice = this.basicTestConnection(security, null,
deviceCredentials,
clientEndpoint,
transportConfiguration,
@ -121,8 +119,7 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITH_ONE_OBSERVE, getBootstrapServerCredentialsSecure(PSK, NONE));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false);
String awaitAlias = "await on client state (Psk_Lwm2m)";
Device lwm2mDevice = this.basicTestConnection(security,
null,
Device lwm2mDevice = this.basicTestConnection(security, null,
deviceCredentials,
clientEndpoint,
transportConfiguration,
@ -175,12 +172,12 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes
clientCredentials.setEndpoint(clientEndpoint);
clientCredentials.setIdentity(identity);
clientCredentials.setKey(keyPsk);
Security securityBs = pskBootstrap(SECURE_URI_BS,
Security securityPskBs = pskBootstrap(SECURE_URI_BS,
identity.getBytes(StandardCharsets.UTF_8),
Hex.decodeHex(keyPsk.toCharArray()));
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, BOTH));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false);
this.basicTestConnection(null, securityBs,
this.basicTestConnection(null, securityPskBs,
deviceCredentials,
clientEndpoint,
transportConfiguration,

4
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java

@ -113,13 +113,13 @@ public class RpkLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTes
RPKClientCredential clientCredentials = new RPKClientCredential();
clientCredentials.setEndpoint(clientEndpoint);
clientCredentials.setKey(Base64.encodeBase64String(certificate.getPublicKey().getEncoded()));
Security securityBs = rpkBootstrap(SECURE_URI_BS,
Security securityRpkBs = rpkBootstrap(SECURE_URI_BS,
certificate.getPublicKey().getEncoded(),
privateKey.getEncoded(),
serverX509CertBs.getPublicKey().getEncoded());
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(RPK, BOTH));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, clientPrivateKeyFromCertTrust, certificate, RPK, false);
this.basicTestConnection(null, securityBs,
this.basicTestConnection(null, securityRpkBs,
deviceCredentials,
clientEndpoint,
transportConfiguration,

8
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java

@ -51,15 +51,14 @@ public class X509_NoTrustLwM2MIntegrationTest extends AbstractSecurityLwM2MInteg
X509ClientCredential clientCredentials = new X509ClientCredential();
clientCredentials.setEndpoint(clientEndpoint);
clientCredentials.setCert(Base64.getEncoder().encodeToString(certificate.getEncoded()));
Security security = x509(SECURE_URI,
Security securityX509 = x509(SECURE_URI,
shortServerId,
certificate.getEncoded(),
privateKey.getEncoded(),
serverX509Cert.getEncoded());
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, NONE));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false);
this.basicTestConnection(security,
null,
this.basicTestConnection(securityX509, null,
deviceCredentials,
clientEndpoint,
transportConfiguration,
@ -119,8 +118,7 @@ public class X509_NoTrustLwM2MIntegrationTest extends AbstractSecurityLwM2MInteg
serverX509CertBs.getEncoded());
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, BOTH));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false);
this.basicTestConnection(security,
null,
this.basicTestConnection(security, null,
deviceCredentials,
clientEndpoint,
transportConfiguration,

6
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java

@ -50,8 +50,7 @@ public class X509_TrustLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegra
serverX509Cert.getEncoded());
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, NONE));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false);
this.basicTestConnection(security,
null,
this.basicTestConnection(security, null,
deviceCredentials,
clientEndpoint,
transportConfiguration,
@ -77,8 +76,7 @@ public class X509_TrustLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegra
serverX509CertBs.getEncoded());
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, BOTH));
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false);
this.basicTestConnection(security,
null,
this.basicTestConnection(security,null,
deviceCredentials,
clientEndpoint,
transportConfiguration,

112
common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/Lwm2mServerIdentifier.java

@ -0,0 +1,112 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.device.credentials.lwm2m;
/**
* Enum representing predefined LwM2M Short Server Identifiers.
* <p>
* See OMA Lightweight M2M Specification for details about the server identifier space.
*/
public enum Lwm2mServerIdentifier {
/**
* Not used for identifying an LwM2M Server (0).
*/
NOT_USED_IDENTIFYING_LWM2M_SERVER_MIN(0, "Bootstrap Short Server ID", false),
/**
* Primary LwM2M Server Short Server ID (1).
* Upper boundary for valid LwM2M Server Identifiers (165534).
*/
PRIMARY_LWM2M_SERVER(1, "LwM2M Server Short Server ID", true),
/**
* Maximum valid LwM2M Server ID (65534).
* Upper boundary for valid LwM2M Server Identifiers (165534).
*/
LWM2M_SERVER_MAX(65534, "LwM2M Server Short Server ID", true),
/**
* Not used for identifying an LwM2M Server (65535).
* Reserved sentinel value representing "no server associated" or "invalid ID".
* MUST NOT be assigned to any LwM2M Server according to OMA-TS-LightweightM2M-Core, §6.2.1.
* OMA LwM2M Core / v1.2: Server / Short Server ID): «MAX_ID 65535 is a reserved value and MUST NOT be used for identifying an Object»
*/
NOT_USED_IDENTIFYING_LWM2M_SERVER_MAX(65535, "Reserved sentinel value (no active server)", false);
private final Integer id;
private final String description;
private final boolean isLwm2mServer;
Lwm2mServerIdentifier(Integer id, String description, boolean isLwm2mServer) {
this.id = id;
this.description = description;
this.isLwm2mServer = isLwm2mServer;
}
/**
* @return the integer value of this Short Server ID.
*/
public Integer getId() {
return id;
}
/**
* @return a human-readable description of this Server ID.
*/
public String getDescription() {
return description;
}
/**
* @return true if this ID represents a Lwm2m Server.
*/
public boolean isLwm2mServer() {
return isLwm2mServer;
}
/**
* Checks whether a given ID represents a valid LwM2M Server (165534).
* @param id Short Server ID value.
* @return true if the ID belongs to a standard LwM2M Server.
*/
public static boolean isLwm2mServer(Integer id) {
return id != null && id >= PRIMARY_LWM2M_SERVER.id && id <= LWM2M_SERVER_MAX.id;
}
public static boolean isNotLwm2mServer(Integer id) {
return id == null || id < PRIMARY_LWM2M_SERVER.id || id > LWM2M_SERVER_MAX.id;
}
/**
* Returns a {@link Lwm2mServerIdentifier} instance matching the given ID.
* @param id numeric ID.
* @return corresponding enum constant.
* @throws IllegalArgumentException if no constant matches the given ID.
*/
public static Lwm2mServerIdentifier fromId(Integer id) {
for (Lwm2mServerIdentifier s : values()) {
if (s.id == id) {
return s;
}
}
throw new IllegalArgumentException("Unknown Lwm2mServerIdentifier: " + id);
}
@Override
public String toString() {
return name() + "(" + id + ") - " + description;
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java

@ -26,7 +26,7 @@ public class LwM2MServerSecurityConfig implements Serializable {
@Schema(description = "Server short Id. Used as link to associate server Object Instance. This identifier uniquely identifies each LwM2M Server configured for the LwM2M Client. " +
"This Resource MUST be set when the Bootstrap-Server Resource has a value of 'false'. " +
"The values ID:1 and ID:65534 values MUST NOT be used for identifying the LwM2M Server.", example = "123", accessMode = Schema.AccessMode.READ_ONLY)
"The values ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server.", example = "123", accessMode = Schema.AccessMode.READ_ONLY)
protected Integer shortServerId = 123;
/** Security -> ObjectId = 0 'LWM2M Security' */
@Schema(description = "Is Bootstrap Server or Lwm2m Server. " +

4
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java

@ -127,7 +127,9 @@ public class LwM2MBootstrapConfig implements Serializable {
private BootstrapConfig.ServerConfig setServerConfig (AbstractLwM2MBootstrapServerCredential serverCredential) {
BootstrapConfig.ServerConfig serverConfig = new BootstrapConfig.ServerConfig();
serverConfig.shortId = serverCredential.getShortServerId();
if (serverCredential.getShortServerId() != null) {
serverConfig.shortId = serverCredential.getShortServerId();
}
serverConfig.lifetime = serverCredential.getLifetime();
serverConfig.defaultMinPeriod = serverCredential.getDefaultMinPeriod();
serverConfig.notifIfDisabled = serverCredential.isNotifIfDisabled();

17
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2mDefaultBootstrapSessionManager.java

@ -16,6 +16,7 @@
package org.thingsboard.server.transport.lwm2m.bootstrap.secure;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.coap.Request;
import org.eclipse.leshan.core.peer.IpPeer;
import org.eclipse.leshan.core.peer.LwM2mPeer;
import org.eclipse.leshan.core.peer.PskIdentity;
@ -112,8 +113,10 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession
} catch (InvalidConfigurationException e){
log.error("Failed put to lwM2MBootstrapSessionClients by endpoint [{}]", request.getEndpointName(), e);
}
String msg = String.format("Bootstrap session started... %s", ((Request) request.getCoapRequest()).getLocalAddress().toString());
log.warn(String.format("%s: %s", request.getEndpointName(), msg));
this.sendLogs(request.getEndpointName(),
String.format("%s: Bootstrap session started...", LOG_LWM2M_INFO, request.getEndpointName()));
String.format("%s: %s", LOG_LWM2M_INFO, msg));
}
return session;
}
@ -135,7 +138,7 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession
session.setModel(modelProvider.getObjectModel(session, tasks.supportedObjects));
// set Requests to Send
log.info("tasks.requestsToSend = [{}]", tasks.requestsToSend);
log.warn("tasks.requestsToSend = [{}]", tasks.requestsToSend);
session.setRequests(tasks.requestsToSend);
// prepare list where we will store Responses
@ -182,14 +185,16 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession
session.getResponses().add(response);
String msg = String.format("%s: receives success response for: %s %s %s", LOG_LWM2M_INFO,
request.getClass().getSimpleName(), request.getPath().toString(), response.toString());
log.warn(msg);
this.sendLogs(bsSession.getEndpoint(), msg);
// on success for NOT bootstrap finish request we send next request
return BootstrapPolicy.continueWith(nextRequest(bsSession));
} else {
// on success for bootstrap finish request we stop the session
this.sendLogs(bsSession.getEndpoint(),
String.format("%s: receives success response for bootstrap finish.", LOG_LWM2M_INFO));
String msg = String.format("%s: receives success response for bootstrap finish.", LOG_LWM2M_INFO);
log.info(msg);
this.sendLogs(bsSession.getEndpoint(), msg);
this.tasksProvider.remove(bsSession.getEndpoint());
return BootstrapPolicy.finished();
}
@ -228,7 +233,9 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession
@Override
public void end(BootstrapSession bsSession) {
this.sendLogs(bsSession.getEndpoint(), String.format("%s: Bootstrap session finished.", LOG_LWM2M_INFO));
String msg = String.format("%s: Bootstrap session finished.", LOG_LWM2M_INFO);
log.warn(msg);
this.sendLogs(bsSession.getEndpoint(), msg);
this.tasksProvider.remove(bsSession.getEndpoint());
}

326
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java

@ -17,15 +17,12 @@ package org.thingsboard.server.transport.lwm2m.bootstrap.store;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.link.Link;
import org.eclipse.leshan.core.node.LwM2mObject;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.request.BootstrapDeleteRequest;
import org.eclipse.leshan.core.request.BootstrapDiscoverRequest;
import org.eclipse.leshan.core.request.BootstrapDownlinkRequest;
import org.eclipse.leshan.core.request.BootstrapReadRequest;
import org.eclipse.leshan.core.request.ContentFormat;
import org.eclipse.leshan.core.response.BootstrapDiscoverResponse;
import org.eclipse.leshan.core.response.BootstrapReadResponse;
import org.eclipse.leshan.core.response.LwM2mResponse;
import org.eclipse.leshan.server.bootstrap.BootstrapConfig;
import org.eclipse.leshan.server.bootstrap.BootstrapConfigStore;
@ -33,7 +30,6 @@ import org.eclipse.leshan.server.bootstrap.BootstrapSession;
import org.eclipse.leshan.server.bootstrap.BootstrapUtil;
import org.eclipse.leshan.server.bootstrap.InvalidConfigurationException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@ -43,14 +39,18 @@ import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;
import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE;
import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL;
import static org.eclipse.leshan.core.LwM2mId.SECURITY;
import static org.eclipse.leshan.core.LwM2mId.SERVER;
import static org.eclipse.leshan.server.bootstrap.BootstrapUtil.toWriteRequest;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.BOOTSTRAP_DEFAULT_SHORT_ID_0;
import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.LWM2M_SERVER_MAX;
import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.PRIMARY_LWM2M_SERVER;
import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.isLwm2mServer;
@Slf4j
public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTaskProvider {
@ -77,11 +77,11 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask
@Override
public Tasks getTasks(BootstrapSession session, List<LwM2mResponse> previousResponse) {
// BootstrapConfig config = store.get(session.getEndpoint(), session.getClientTransportData().getIdentity(), session);
BootstrapConfig config = store.get(session);
if (config == null) {
BootstrapConfig configNew = store.get(session);
if (configNew == null) {
return null;
}
if (previousResponse == null && shouldStartWithDiscover(config)) {
if (previousResponse == null && shouldStartWithDiscover(configNew)) {
Tasks tasks = new Tasks();
tasks.requestsToSend = new ArrayList<>(1);
tasks.requestsToSend.add(new BootstrapDiscoverRequest());
@ -96,47 +96,27 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask
tasks.supportedObjects = this.supportedObjects;
// handle bootstrap discover response
if (previousResponse != null) {
if (previousResponse.get(0) instanceof BootstrapDiscoverResponse) {
BootstrapDiscoverResponse discoverResponse = (BootstrapDiscoverResponse) previousResponse.get(0);
if (previousResponse.get(0) instanceof BootstrapDiscoverResponse discoverResponse) {
if (discoverResponse.isSuccess()) {
this.initAfterBootstrapDiscover(discoverResponse);
findSecurityInstanceId(discoverResponse.getObjectLinks(), session.getEndpoint());
} else {
this.initAfterBootstrapDiscover(discoverResponse);
/// Short Server Ids - in old config
findInstancesIdOldByServerId(discoverResponse, session.getEndpoint());
log.warn(
"Bootstrap Discover return error {} : to continue bootstrap session without autoIdForSecurityObject mode. {}",
"Bootstrap server instance successfully found in Security Object (0) in response {}. Continuing bootstrap session. Session: {}",
discoverResponse, session);
}
if (this.lwM2MBootstrapSessionClients.get(session.getEndpoint()).getSecurityInstances().get(BOOTSTRAP_DEFAULT_SHORT_ID_0) == null) {
log.error(
"Unable to find bootstrap server instance in Security Object (0) in response {}: unable to continue bootstrap session with autoIdForSecurityObject mode. {}",
} else {
log.warn(
"Unable to find bootstrap server instance in Security Object (0) in response {}. Continuing bootstrap session with autoIdForSecurityObject mode, ignoring information from discoverResponse. Session: {}",
discoverResponse, session);
return null;
}
tasks.requestsToSend = new ArrayList<>(1);
tasks.requestsToSend.add(new BootstrapReadRequest("/1"));
tasks.last = false;
return tasks;
}
BootstrapReadResponse readResponse = (BootstrapReadResponse) previousResponse.get(0);
Integer bootstrapServerIdOld = null;
if (readResponse.isSuccess()) {
findServerInstanceId(readResponse, session.getEndpoint());
if (this.lwM2MBootstrapSessionClients.get(session.getEndpoint()).getSecurityInstances().size() > 0 && this.lwM2MBootstrapSessionClients.get(session.getEndpoint()).getServerInstances().size() > 0) {
bootstrapServerIdOld = this.findBootstrapServerId(session.getEndpoint());
}
} else {
log.warn(
"Bootstrap ReadResponse return error {} : to continue bootstrap session without find Server Instance Id. {}",
readResponse, session);
}
// create requests from config
tasks.requestsToSend = this.toRequests(config,
config.contentFormat != null ? config.contentFormat : session.getContentFormat(),
bootstrapServerIdOld, session.getEndpoint());
tasks.requestsToSend = this.toRequests(configNew,
configNew.contentFormat != null ? configNew.contentFormat : session.getContentFormat(), session.getEndpoint());
} else {
// create requests from config
tasks.requestsToSend = BootstrapUtil.toRequests(config,
config.contentFormat != null ? config.contentFormat : session.getContentFormat());
tasks.requestsToSend = BootstrapUtil.toRequests(configNew,
configNew.contentFormat != null ? configNew.contentFormat : session.getContentFormat());
}
return tasks;
}
@ -148,81 +128,57 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask
/**
* "Short Server ID": This Resource MUST be set when the Bootstrap-Server Resource has a value of 'false'.
* The values ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server.
* "Short Server ID":
* "Short Lwm2m Server ID":
* - Link Instance (lwm2m Server) hase linkParams with key = "ssid" value = "shortId" (ver lvm2m = 1.1).
* - Link Instance (bootstrap Server) hase not linkParams with key = "ssid" (ver lvm2m = 1.0).
* The values ID:0 values MUST NOT be used for identifying the LwM2M Server only BS.
*/
protected void findSecurityInstanceId(Link[] objectLinks, String endpoint) {
log.info("Object after discover: [{}]", objectLinks);
for (Link link : objectLinks) {
if (link.getUriReference().startsWith("/0/")) {
try {
LwM2mPath path = new LwM2mPath(link.getUriReference());
if (path.isObjectInstance()) {
if (link.getAttributes().get("ssid") != null) {
int serverId = Integer.parseInt(link.getAttributes().get("ssid").getCoreLinkValue());
if (!lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(serverId)) {
lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(serverId, path.getObjectInstanceId());
} else {
log.error("Invalid lwm2mSecurityInstance by [{}]", path.getObjectInstanceId());
}
lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(serverId, path.getObjectInstanceId());
protected void findInstancesIdOldByServerId(BootstrapDiscoverResponse discoverResponses, String endpoint) {
log.info("Object after discover: [{}]", Arrays.toString(discoverResponses.getObjectLinks()));
for (Link link : discoverResponses.getObjectLinks()) {
LwM2mPath path = new LwM2mPath(link.getUriReference());
if (path.isObjectInstance()) {
int lwm2mShortServerId = 0;
if (path.getObjectId() == 0) {
if (link.getAttributes().get("ssid") != null) {
lwm2mShortServerId = Integer.parseInt(link.getAttributes().get("ssid").getCoreLinkValue());
if (validateLwm2mShortServerId(lwm2mShortServerId)) {
this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().putIfAbsent(lwm2mShortServerId, path.getObjectInstanceId());
} else {
log.error("Invalid lwm2mSecurityInstance [{}] by short server id [{}]", path.getObjectInstanceId(), lwm2mShortServerId);
}
} else {
this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().putIfAbsent(null, path.getObjectInstanceId());
}
} else if (path.getObjectId() == 1) {
if (link.getAttributes().get("ssid") != null) {
lwm2mShortServerId = Integer.parseInt(link.getAttributes().get("ssid").getCoreLinkValue());
if (validateLwm2mShortServerId(lwm2mShortServerId)) {
this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().putIfAbsent(lwm2mShortServerId, path.getObjectInstanceId());
} else {
if (!this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(0)) {
this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(BOOTSTRAP_DEFAULT_SHORT_ID_0, path.getObjectInstanceId());
} else {
log.error("Invalid bootstrapSecurityInstance by [{}]", path.getObjectInstanceId());
}
log.error("Invalid lwm2mServerInstance [{}] by short server id [{}]", path.getObjectInstanceId(), lwm2mShortServerId);
}
}
} catch (Exception e) {
// ignore if this is not a LWM2M path
log.error("Invalid LwM2MPath starting by \"/0/\"");
}
}
}
}
protected void findServerInstanceId(BootstrapReadResponse readResponse, String endpoint) {
try {
((LwM2mObject) readResponse.getContent()).getInstances().values().forEach(instance -> {
var shId = OPAQUE.equals(instance.getResource(0).getType()) ? new BigInteger((byte[]) instance.getResource(0).getValue()).intValue() : instance.getResource(0).getValue();
int shortId;
if (shId instanceof Long) {
shortId = ((Long) shId).intValue();
} else {
shortId = (int) shId;
}
this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().put(shortId, instance.getId());
});
} catch (Exception e) {
log.error("Failed find Server Instance Id. ", e);
}
}
protected Integer findBootstrapServerId(String endpoint) {
Integer bootstrapServerIdOld = null;
Map<Integer, Integer> filteredMap = this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().entrySet()
.stream().filter(x -> !this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(x.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
if (filteredMap.size() > 0) {
bootstrapServerIdOld = filteredMap.keySet().stream().findFirst().get();
}
return bootstrapServerIdOld;
}
public BootstrapConfigStore getStore() {
return this.store;
}
private void initAfterBootstrapDiscover(BootstrapDiscoverResponse response) {
Link[] links = response.getObjectLinks();
AtomicReference<String> verDefault = new AtomicReference<>("1.0");
Arrays.stream(links).forEach(link -> {
LwM2mPath path = new LwM2mPath(link.getUriReference());
if (!path.isRoot() && path.getObjectId() < 3) {
if (path.isRoot()) {
if (link.hasAttribute() && link.getAttributes().get("lwm2m") != null) {
verDefault.set(link.getAttributes().get("lwm2m").getValue().toString());
}
} else if (path.getObjectId() <= ACCESS_CONTROL) {
if (path.isObject()) {
String ver = link.getAttributes().get("ver") != null ? link.getAttributes().get("ver").getCoreLinkValue() : "1.0";
String ver = (link.hasAttribute() && link.getAttributes().get("ver") != null) ? link.getAttributes().get("ver").getCoreLinkValue() : verDefault.get();
this.supportedObjects.put(path.getObjectId(), ver);
}
}
@ -230,94 +186,124 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask
}
public List<BootstrapDownlinkRequest<? extends LwM2mResponse>> toRequests(BootstrapConfig bootstrapConfig,
/** Map<serverId ("Short Server ID"), InstanceId> => LwM2MBootstrapClientInstanceIds
* 1) Both
* - (Short) Server ID == null bs)
* SECURITY = 0; InstanceId = 0
* - Short Server ID == 1 - 65534 lwm2m)
* SECURITY = 0; InstanceId = 1
* SERVER = 1; InstanceId = 0
* 2) Only BS Server
* - Short Server ID == null bs)
* SECURITY = 0; InstanceId = 0
* 3) Only Lwm2m Server
* - Short Server ID == 1 - 65534 lwm2m)
* SECURITY = 0; InstanceId = 0
* SERVER = 1; InstanceId = 0
* */
public List<BootstrapDownlinkRequest<? extends LwM2mResponse>> toRequests(BootstrapConfig bootstrapConfigNew,
ContentFormat contentFormat,
Integer bootstrapServerIdOld,
String endpoint) {
Integer bootstrapSecurityInstanceId = this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(null) == null ?
-2 : this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(null);
List<BootstrapDownlinkRequest<? extends LwM2mResponse>> requests = new ArrayList<>();
Set<String> pathsDelete = new HashSet<>();
List<BootstrapDownlinkRequest<? extends LwM2mResponse>> requestsWrite = new ArrayList<>();
boolean isBsServer = false;
boolean isLwServer = false;
/** Map<serverId ("Short Server ID"), InstanceId> */
Map<Integer, Integer> instances = new HashMap<>();
Integer bootstrapServerIdNew = null;
// handle security
int lwm2mSecurityInstanceId = 0;
int bootstrapSecurityInstanceId = this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(BOOTSTRAP_DEFAULT_SHORT_ID_0);
for (BootstrapConfig.ServerSecurity security : new TreeMap<>(bootstrapConfig.security).values()) {
if (security.bootstrapServer) {
requestsWrite.add(toWriteRequest(bootstrapSecurityInstanceId, security, contentFormat));
isBsServer = true;
bootstrapServerIdNew = security.serverId;
instances.put(security.serverId, bootstrapSecurityInstanceId);
} else {
if (lwm2mSecurityInstanceId == bootstrapSecurityInstanceId) {
lwm2mSecurityInstanceId++;
}
requestsWrite.add(toWriteRequest(lwm2mSecurityInstanceId, security, contentFormat));
instances.put(security.serverId, lwm2mSecurityInstanceId);
isLwServer = true;
if (!isBsServer && this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(security.serverId) &&
lwm2mSecurityInstanceId != this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(security.serverId)) {
pathsDelete.add("/0/" + this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(security.serverId));
}
/**
* If there is an instance in the serverInstances with serverId which we replace in the securityInstances
*/
// find serverId in securityInstances by id (instance)
Integer serverIdOld = null;
for (Map.Entry<Integer, Integer> entry : this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().entrySet()) {
if (entry.getValue().equals(lwm2mSecurityInstanceId)) {
serverIdOld = entry.getKey();
}
}
if (!isBsServer && serverIdOld != null && this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().containsKey(serverIdOld)) {
pathsDelete.add("/1/" + this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(serverIdOld));
}
lwm2mSecurityInstanceId++;
ConcurrentHashMap<String, BootstrapDownlinkRequest<? extends LwM2mResponse>> requestsWrite = new ConcurrentHashMap<>();
/// handle security & handle
// bootstrap Security new - There can only be one instance of bootstrap at a time.
/// bs: handle security only
for (BootstrapConfig.ServerSecurity security : new TreeMap<>(bootstrapConfigNew.security).values()) {
if (security.bootstrapServer && bootstrapSecurityInstanceId > -1) {
// delete old bootstrap Security
String path = "/" + SECURITY + "/" + bootstrapSecurityInstanceId;
pathsDelete.add(path);
security.serverId = null;
requestsWrite.put(path, toWriteRequest(bootstrapSecurityInstanceId, security, contentFormat));
}
}
// handle server
for (Map.Entry<Integer, BootstrapConfig.ServerConfig> server : bootstrapConfig.servers.entrySet()) {
int securityInstanceId = instances.get(server.getValue().shortId);
requestsWrite.add(toWriteRequest(securityInstanceId, server.getValue(), contentFormat));
if (!isBsServer) {
/** Delete instance if bootstrapServerIdNew not equals bootstrapServerIdOld or securityInstanceBsIdNew not equals serverInstanceBsIdOld */
if (bootstrapServerIdNew != null && server.getValue().shortId == bootstrapServerIdNew &&
(bootstrapServerIdNew != bootstrapServerIdOld || securityInstanceId != this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(bootstrapServerIdOld))) {
pathsDelete.add("/1/" + this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(bootstrapServerIdOld));
/** Delete instance if serverIdNew is present in serverInstances and securityInstanceIdOld by serverIdNew not equals serverInstanceIdOld */
} else if (this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().containsKey(server.getValue().shortId) &&
securityInstanceId != this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(server.getValue().shortId)) {
pathsDelete.add("/1/" + this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(server.getValue().shortId));
}
/** lwm2m servers: Multiple instances of lwm2m servers can run simultaneously by SHORT_ID
if update -> delete and write by InstanceId
if new -> only write with InstanceIdMax++
*/
/// lwm2m server: handle security & server
//max Lwm2m Security instance old id if new
int lwm2mSecurityInstanceIdMax = -1;
for (Integer shortId : this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().keySet()) {
if (isLwm2mServer(shortId)) {
lwm2mSecurityInstanceIdMax = Math.max(
this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(shortId),
lwm2mSecurityInstanceIdMax);
}
}
// handle acl
for (Map.Entry<Integer, BootstrapConfig.ACLConfig> acl : bootstrapConfig.acls.entrySet()) {
requestsWrite.add(toWriteRequest(acl.getKey(), acl.getValue(), contentFormat));
//max Lwm2m Server instance old id if new
int lwm2mServerInstanceIdMax = -1;
for (Integer shortId : this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().keySet()) {
if (isLwm2mServer(shortId)) {
lwm2mServerInstanceIdMax = Math.max(
this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(shortId),
lwm2mServerInstanceIdMax);
}
}
// handle delete
if (isBsServer && isLwServer) {
requests.add(new BootstrapDeleteRequest("/0"));
requests.add(new BootstrapDeleteRequest("/1"));
} else {
pathsDelete.forEach(pathDelete -> requests.add(new BootstrapDeleteRequest(pathDelete)));
// Lwm2m update or new
for (BootstrapConfig.ServerSecurity security : new TreeMap<>(bootstrapConfigNew.security).values()) {
if (!security.bootstrapServer) {
// Security
Integer secureInstanceId = this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(security.serverId);
if (secureInstanceId != null) {
pathsDelete.add("/" + SECURITY + "/" + secureInstanceId);
requestsWrite.put("/" + SECURITY + "/" + secureInstanceId, toWriteRequest(secureInstanceId, security, contentFormat));
} else {
secureInstanceId = ++lwm2mSecurityInstanceIdMax;
if (bootstrapSecurityInstanceId.equals(secureInstanceId)) {
secureInstanceId = ++lwm2mSecurityInstanceIdMax;
}
requestsWrite.put("/" + SECURITY + "/" + secureInstanceId, toWriteRequest(secureInstanceId, security, contentFormat));
}
Integer serverInstanceId = this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(security.serverId);
if (serverInstanceId != null) {
pathsDelete.add("/" + SERVER + "/" + serverInstanceId);
} else {
serverInstanceId = ++lwm2mServerInstanceIdMax;
}
Integer finalServerInstanceId = serverInstanceId;
new TreeMap<>(bootstrapConfigNew.servers).values().stream()
.filter(server -> server.shortId == security.serverId)
.findFirst()
.ifPresent(server ->
requestsWrite.put(
"/" + SERVER + "/" + finalServerInstanceId,
toWriteRequest(finalServerInstanceId, server, contentFormat)
)
);
}
}
// handle write
if (requestsWrite.size() > 0) {
requests.addAll(requestsWrite);
/// handle acl
for (Map.Entry<Integer, BootstrapConfig.ACLConfig> acl : bootstrapConfigNew.acls.entrySet()) {
requestsWrite.put("/" + ACCESS_CONTROL + "/" + acl.getKey(), toWriteRequest(acl.getKey(), acl.getValue(), contentFormat));
}
/// handle delete
pathsDelete.forEach(pathDelete -> requests.add(new BootstrapDeleteRequest(pathDelete)));
/// handle write
if (!requestsWrite.isEmpty()) {
requests.addAll(requestsWrite.values());
}
return (requests);
}
private void initSupportedObjectsDefault() {
this.supportedObjects = new HashMap<>();
this.supportedObjects.put(0, "1.1");
this.supportedObjects.put(1, "1.1");
this.supportedObjects.put(2, "1.0");
this.supportedObjects.put(SECURITY, "1.1");
this.supportedObjects.put(SERVER, "1.1");
this.supportedObjects.put(ACCESS_CONTROL, "1.0");
}
private boolean validateLwm2mShortServerId(int id){
return id >= PRIMARY_LWM2M_SERVER.getId() && id <= LWM2M_SERVER_MAX.getId();
}
@Override

11
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MConfigurationChecker.java

@ -21,6 +21,10 @@ import org.eclipse.leshan.server.bootstrap.InvalidConfigurationException;
import java.util.Map;
import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.NOT_USED_IDENTIFYING_LWM2M_SERVER_MIN;
import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.NOT_USED_IDENTIFYING_LWM2M_SERVER_MAX;
import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.isNotLwm2mServer;
public class LwM2MConfigurationChecker extends ConfigurationChecker {
@Override
@ -74,15 +78,16 @@ public class LwM2MConfigurationChecker extends ConfigurationChecker {
* This Resource MUST be set when the Bootstrap-Server Resource has false value.
* Specific ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server (Section 6.3 of the LwM2M version 1.0 specification).
*/
if (!security.bootstrapServer && (srvCfg.shortId < 1 && srvCfg.shortId > 65534 )) {
throw new InvalidConfigurationException("Specific ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server");
if (!security.bootstrapServer && isNotLwm2mServer(srvCfg.shortId)) {
throw new InvalidConfigurationException("Specific ID:" + NOT_USED_IDENTIFYING_LWM2M_SERVER_MIN.getId() + " and ID:" + NOT_USED_IDENTIFYING_LWM2M_SERVER_MAX.getId() + " values MUST NOT be used for identifying the LwM2M Server");
}
}
}
protected static BootstrapConfig.ServerSecurity getSecurityEntry(BootstrapConfig config, int shortId) {
for (Map.Entry<Integer, BootstrapConfig.ServerSecurity> es : config.security.entrySet()) {
if (es.getValue().serverId == shortId) {
if ((es.getValue().serverId == null && shortId == 0) ||
(es.getValue().serverId != null && es.getValue().serverId == shortId)) {
return es.getValue();
}
}

6
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java

@ -66,7 +66,9 @@ import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.BOOTSTRAP_TRIGGER_PARAMS_ID;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.LWM2M_OBJECT_VERSION_DEFAULT;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.REGISTRATION_TRIGGER_PARAMS_ID;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertMultiResourceValuesFromRpcBody;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.equalsResourceTypeGetSimpleName;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId;
@ -342,6 +344,10 @@ public class LwM2mClient {
public String isValidObjectVersion(String path) {
LwM2mPath pathIds = getLwM2mPathFromString(path);
if (pathIds.isResource() && (pathIds.toString().equals(REGISTRATION_TRIGGER_PARAMS_ID ) ||
pathIds.toString().equals(BOOTSTRAP_TRIGGER_PARAMS_ID))) {
return "";
}
LwM2m.Version verSupportedObject = this.getSupportedObjectVersion(pathIds.getObjectId());
if (verSupportedObject == null) {
return String.format("Specified object id %s absent in the list supported objects of the client or is security object!", pathIds.getObjectId());

3
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java

@ -864,7 +864,8 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
ResourceUpdateResult updateResource = new ResourceUpdateResult(lwM2MClient);
request.getObjectInstances().forEach(instance ->
instance.getResources().forEach((resId, lwM2mResource) ->{
this.updateResourcesValue(updateResource, lwM2mResource, versionId + "/" + resId, Mode.REPLACE, 0);
String path = versionId.endsWith("/") ? versionId + resId : versionId + "/" + resId;
this.updateResourcesValue(updateResource, lwM2mResource, path, Mode.REPLACE, 0);
})
);
clientContext.update(lwM2MClient);

3
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java

@ -81,7 +81,8 @@ public class LwM2MTransportUtil {
public static final String LOG_LWM2M_INFO = "info";
public static final String LOG_LWM2M_ERROR = "error";
public static final String LOG_LWM2M_WARN = "warn";
public static final int BOOTSTRAP_DEFAULT_SHORT_ID_0 = 0;
public static final String REGISTRATION_TRIGGER_PARAMS_ID = "/1/0/8";
public static final String BOOTSTRAP_TRIGGER_PARAMS_ID = "/1/0/9";;
public static LwM2mOtaConvert convertOtaUpdateValueToString(String pathIdVer, Object value, ResourceModel.Type currentType) {
String path = fromVersionedIdToObjectId(pathIdVer);

27
dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java

@ -69,6 +69,10 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.LWM2M_SERVER_MAX;
import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.PRIMARY_LWM2M_SERVER;
import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.isNotLwm2mServer;
@Slf4j
@Component
public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<DeviceProfile> {
@ -337,19 +341,22 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
throw new DeviceCredentialsValidationException("Bootstrap config must not include \"Bootstrap Server\". \"Include Bootstrap Server updates\" is " + isBootstrapServerUpdateEnable + ".");
}
if (serverConfig.getShortServerId() != null) {
if (serverConfig.isBootstrapServerIs()) {
if (serverConfig.getShortServerId() < 0 || serverConfig.getShortServerId() > 65535) {
throw new DeviceCredentialsValidationException("Bootstrap Server ShortServerId must be in range [0 - 65535]!");
}
} else {
if (serverConfig.getShortServerId() < 1 || serverConfig.getShortServerId() > 65534) {
throw new DeviceCredentialsValidationException("LwM2M Server ShortServerId must be in range [1 - 65534]!");
if (serverConfig.isBootstrapServerIs()){
if (serverConfig.getShortServerId() != null) {
if (serverConfig.getShortServerId() == 0) {
serverConfig.setShortServerId(null);
} else {
throw new DeviceCredentialsValidationException("Bootstrap Server ShortServerId must be null!");
}
}
} else {
String serverName = serverConfig.isBootstrapServerIs() ? "Bootstrap Server" : "LwM2M Server";
throw new DeviceCredentialsValidationException(serverName + " ShortServerId must not be null!");
if (serverConfig.getShortServerId() != null) {
if (isNotLwm2mServer(serverConfig.getShortServerId())) {
throw new DeviceCredentialsValidationException("LwM2M Server ShortServerId must be in range [" + PRIMARY_LWM2M_SERVER.getId() + " - " + LWM2M_SERVER_MAX.getId() + "]!");
}
} else {
throw new DeviceCredentialsValidationException("LwM2M Server ShortServerId must not be null!");
}
}
String server = serverConfig.isBootstrapServerIs() ? "Bootstrap Server" : "LwM2M Server";

20
dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java

@ -48,6 +48,9 @@ import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.verify;
import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.LWM2M_SERVER_MAX;
import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.NOT_USED_IDENTIFYING_LWM2M_SERVER_MAX;
import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.PRIMARY_LWM2M_SERVER;
@SpringBootTest(classes = DeviceProfileDataValidator.class)
class DeviceProfileDataValidatorTest {
@ -74,8 +77,8 @@ class DeviceProfileDataValidatorTest {
" \"clientOnlyObserveAfterConnect\": 1\n" +
" }";
private static final String msgErrorLwm2mRange = "LwM2M Server ShortServerId must be in range [1 - 65534]!";
private static final String msgErrorBsRange = "Bootstrap Server ShortServerId must be in range [0 - 65535]!";
private static final String msgErrorLwm2mRange = "LwM2M Server ShortServerId must be in range [" + PRIMARY_LWM2M_SERVER.getId() + " - " + LWM2M_SERVER_MAX.getId() + "]!";
private static final String msgErrorBsRange = "Bootstrap Server ShortServerId must be null!";
private static final String msgErrorNotNull = " Server ShortServerId must not be null!";
private static final String host = "localhost";
private static final String hostBs = "localhost";
@ -124,7 +127,7 @@ class DeviceProfileDataValidatorTest {
@Test
void testValidateDeviceProfile_Lwm2mBootstrap_ShortServerId_Ok() {
Integer shortServerId = 123;
Integer shortServerIdBs = 0;
Integer shortServerIdBs = null;
DeviceProfile deviceProfile = getDeviceProfile(shortServerId, shortServerIdBs);
validator.validateDataImpl(tenantId, deviceProfile);
@ -132,8 +135,13 @@ class DeviceProfileDataValidatorTest {
}
@Test
void testValidateDeviceProfile_Lwm2mShortServerId_Ok_BootstrapShortServerId_null_Error() {
verifyValidationError(123, null, "Bootstrap" + msgErrorNotNull);
void testValidateDeviceProfile_Lwm2mShortServerId_Ok_BootstrapShortServerId_validate_0_to_null_Ok() {
Integer shortServerId = 123;
Integer shortServerIdBs = 0;
DeviceProfile deviceProfile = getDeviceProfile(shortServerId, shortServerIdBs);
validator.validateDataImpl(tenantId, deviceProfile);
verify(validator).validateString("Device profile name", deviceProfile.getName());
}
@Test
@ -153,7 +161,7 @@ class DeviceProfileDataValidatorTest {
@Test
void testValidateDeviceProfile_Lwm2mShortServerId_More_65534_Error_BootstrapShortServerId_Ok() {
verifyValidationError(65535, 111, msgErrorLwm2mRange);
verifyValidationError(NOT_USED_IDENTIFYING_LWM2M_SERVER_MAX.getId(), 111, msgErrorLwm2mRange);
}
@Test

70
ui-ngx/src/app/core/http/device.service.ts

@ -16,7 +16,8 @@
import { Injectable } from '@angular/core';
import { createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable, ReplaySubject } from 'rxjs';
import { catchError, Observable, of, ReplaySubject, throwError, timeout } from 'rxjs';
import { map, switchMap } from "rxjs/operators";
import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data';
@ -225,4 +226,71 @@ export class DeviceService {
public downloadGatewayDockerComposeFile(deviceId: string): Observable<any> {
return this.resourcesService.downloadResource(`/api/device-connectivity/gateway-launch/${deviceId}/docker-compose/download`);
}
public rebootDevice(deviceId: string, isBootstrapServer: boolean, config?: RequestConfig): Observable<{
result: string,
msg: string
}> {
const rebootName = isBootstrapServer ? 'Bootstrap-Request Trigger' : 'Registration Update Trigger';
return this.sendTwoWayRpcCommand(deviceId, {method: 'DiscoverAll'}, config).pipe(
timeout(10000),
switchMap((response: any) => {
if (response.result && response.result.toUpperCase() === 'CONTENT') {
const resourceId = isBootstrapServer ? 9 : 8;
const resourcePath = `/1/0/${resourceId}`;
return this.rebootTrigger(deviceId, resourcePath, config).pipe(
map((responseReboot: any) => {
if (responseReboot.result === 'CHANGED') {
return {
result: 'SUCCESS',
msg: `<b>"${rebootName}"</b> - Started Successfully.`
};
} else {
return {
result: 'ERROR',
msg: `<b>"${rebootName}"</b> failed:<pre>${JSON.stringify(responseReboot, null, 2)}</pre>`
}
}
}),
catchError(err =>
of({
result: 'ERROR',
msg: `<b>"${rebootName}"</b> failed.<br>Error: ${err.message || err}`
})
)
);
} else {
return of({
result: 'ERROR',
msg: `<b>"${rebootName}"</b> failed.<br>Bad registration device with id = ${deviceId}.<br><b>"DiscoverAll"</b> - RPC result is not "CONTENT"`
});
}
}),
catchError(err =>
of({
result: 'ERROR',
msg: `<b>"${rebootName}"</b> failed.<br>Bad registration device with id = ${deviceId}.<br>Error: ${err.message || err}`
})
)
);
}
private rebootTrigger(deviceId: string, resourcePath: string, config?: RequestConfig): Observable<{ result: string, msg?: string }> {
return this.sendTwoWayRpcCommand(deviceId, {method: 'Execute', params: {id: resourcePath}}, config).pipe(
timeout(10000),
map(res => {
if (res?.result?.toUpperCase() === 'CHANGED') {
return {result: 'CHANGED'};
} else {
return {
result: `${res?.result}`,
msg: `${res?.error}`
}
}
}),
catchError(err => {
return throwError(() => err);
})
);
}
}

14
ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<mat-tab-group [formGroup]="lwm2mConfigFormGroup">
<mat-tab-group [formGroup]="lwm2mConfigFormGroup" dynamicHeight>
<mat-tab label="{{ 'device.lwm2m-security-config.client-tab' | translate }}">
<ng-container formGroupName="client">
<mat-form-field class="mat-block">
@ -72,6 +72,12 @@
</textarea>
<mat-hint translate>device.lwm2m-security-config.client-public-key-hint</mat-hint>
</mat-form-field>
<button *ngIf="deviceId"
mat-raised-button color="primary" type="button"
(click)="rebootDevice(false)"
[disabled]="lwm2mConfigFormGroup.get('client').invalid">
{{ 'device.lwm2m-security-config.client-reboot' | translate }}
</button>
</ng-container>
</mat-tab>
<mat-tab label="{{ 'device.lwm2m-security-config.bootstrap-tab' | translate }}">
@ -103,5 +109,11 @@
</mat-expansion-panel>
</mat-accordion>
</div>
<button *ngIf="deviceId"
mat-raised-button color="primary" style="margin-top: 22px" type="button"
(click)="rebootDevice(true)"
[disabled]="lwm2mConfigFormGroup.get('client').invalid">
{{ 'device.lwm2m-security-config.bootstrap-reboot' | translate }}
</button>
</mat-tab>
</mat-tab-group>

56
ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts

@ -14,20 +14,21 @@
/// limitations under the License.
///
import { Component, forwardRef, OnDestroy } from '@angular/core';
import { Component, forwardRef, Input, OnDestroy } from '@angular/core';
import {
ControlValueAccessor,
UntypedFormBuilder,
UntypedFormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import {
getDefaultClientSecurityConfig,
getDefaultServerSecurityConfig, Lwm2mClientKeyTooltipTranslationsMap,
getDefaultServerSecurityConfig,
Lwm2mClientKeyTooltipTranslationsMap,
Lwm2mSecurityConfigModels,
Lwm2mSecurityType,
Lwm2mSecurityTypeTranslationMap
@ -35,6 +36,11 @@ import {
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { isDefinedAndNotNull } from '@core/utils';
import { DeviceId } from "@shared/models/id/device-id";
import { DeviceService } from "@core/http/device.service";
import { ActionNotificationShow } from "@core/notification/notification.actions";
import { Store } from "@ngrx/store";
import { AppState } from "@core/core.state";
@Component({
selector: 'tb-device-credentials-lwm2m',
@ -65,7 +71,12 @@ export class DeviceCredentialsLwm2mComponent implements ControlValueAccessor, Va
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => {};
constructor(private fb: UntypedFormBuilder) {
@Input()
deviceId: DeviceId;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder,
private deviceService: DeviceService) {
this.lwm2mConfigFormGroup = this.initLwm2mConfigForm();
}
@ -101,6 +112,41 @@ export class DeviceCredentialsLwm2mComponent implements ControlValueAccessor, Va
this.destroy$.complete();
}
/**
* AbstractRpcController -> rpcController
* - API
* "/api/plugins/rpc/twoway/${this.deviceId.id}"
* - DiscoveryAll
* requestBody = "{\"method\":\"DiscoverAll\"}";
* - "Registration Update Trigger",
* requestBody = "{\"method\": \"Execute\", \"params\": {\"id\": \"/1/0/8\"}}
* - "Bootstrap-Request Trigger"
* requestBody = "{\"method\": \"Execute\", \"params\": {\"id\": \"/1/0/9\"}}
*/
public rebootDevice(isBootstrapServer: boolean): void {
this.deviceService.rebootDevice(this.deviceId.id, isBootstrapServer).subscribe(responseReboot => {
if (responseReboot.result === 'SUCCESS') {
this.store.dispatch(new ActionNotificationShow(
{
message: responseReboot.msg,
type: 'success',
duration: 1500,
verticalPosition: 'top',
horizontalPosition: 'left'
}));
} else {
this.store.dispatch(new ActionNotificationShow(
{
message: responseReboot.msg,
type: 'error',
verticalPosition: 'top',
horizontalPosition: 'left'
}));
}
});
}
private initClientSecurityConfig(config: Lwm2mSecurityConfigModels): void {
this.lwm2mConfigFormGroup.patchValue(config, {emitEvent: false});
this.securityConfigClientUpdateValidators(config.client.securityConfigClientMode);

3
ui-ngx/src/app/modules/home/components/device/device-credentials.component.html

@ -81,7 +81,8 @@
</tb-device-credentials-mqtt-basic>
</ng-template>
<ng-template [ngSwitchCase]="deviceCredentialsType.LWM2M_CREDENTIALS">
<tb-device-credentials-lwm2m formControlName="credentialsValue">
<tb-device-credentials-lwm2m formControlName="credentialsValue"
[deviceId]="deviceId">
</tb-device-credentials-lwm2m>
</ng-template>
</div>

4
ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts

@ -36,6 +36,7 @@ import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { generateSecret, isDefinedAndNotNull } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
import { DeviceId } from "@shared/models/id/device-id";
@Component({
selector: 'tb-device-credentials',
@ -88,6 +89,8 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
credentialTypeNamesMap = credentialTypeNames;
deviceId: DeviceId;
private propagateChange = null;
private propagateChangePending = false;
@ -126,6 +129,7 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
writeValue(value: DeviceCredentials | null): void {
if (isDefinedAndNotNull(value)) {
this.deviceId = value.deviceId;
const credentialsType = this.credentialsTypes.includes(value.credentialsType) ? value.credentialsType : this.credentialsTypes[0];
this.deviceCredentialsFormGroup.patchValue({
credentialsType,

10
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html

@ -24,7 +24,7 @@
'device-profile.lwm2m.bootstrap-server' : 'device-profile.lwm2m.lwm2m-server') | translate }}</div>
<div *ngIf="!serverPanel.expanded" style="font-size:14px" class="no-wrap flex flex-row">
<div style="margin-left:32px">{{ ('device-profile.lwm2m.short-id' | translate) + ': ' }}
<span style="font-style: italic">{{ serverFormGroup.get('shortServerId').value }}</span>
<span style="font-style: italic">{{ serverFormGroup.get('shortServerId').value ? serverFormGroup.get('shortServerId').value : '' }}</span>
</div>
<div style="margin-left:32px">{{ ('device-profile.lwm2m.mode' | translate) + ': ' }}
<span style="font-style: italic">{{ credentialTypeLwM2MNamesMap.get(securityConfigLwM2MType[serverFormGroup.get('securityMode').value]) }}</span>
@ -54,16 +54,18 @@
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex-1">
<mat-form-field class="flex-1" *ngIf="!isBootstrap">
<mat-label>{{ 'device-profile.lwm2m.short-id' | translate }}</mat-label>
<mat-icon *ngIf="!disabled" class="mat-primary" aria-hidden="false" aria-label="help-icon" matSuffix style="cursor:pointer;"
matTooltip="{{ (isBootstrap ? 'device-profile.lwm2m.short-id-tooltip-bootstrap': 'device-profile.lwm2m.short-id-tooltip') | translate }}">help</mat-icon>
<input matInput type="number" [min]="shortServerIdMin" [max]="shortServerIdMax" formControlName="shortServerId" required>
<input matInput type="number" formControlName="shortServerId" required
[min]="isBootstrap ? undefined : shortServerIdMin"
[max]="isBootstrap ? undefined : shortServerIdMax">
<mat-error *ngIf="serverFormGroup.get('shortServerId').hasError('required')">
{{ 'device-profile.lwm2m.short-id-required' | translate }}
</mat-error>
<mat-error *ngIf="serverFormGroup.get('shortServerId').hasError('pattern')">
{{ 'device-profile.lwm2m.short-id-pattern' | translate }}
{{ (isBootstrap ? 'device-profile.lwm2m.short-id-pattern-bs' : 'device-profile.lwm2m.short-id-pattern') | translate }}
</mat-error>
<mat-error *ngIf="serverFormGroup.get('shortServerId').hasError('min') ||
serverFormGroup.get('shortServerId').hasError('max')">

14
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts

@ -74,8 +74,8 @@ export class Lwm2mDeviceConfigServerComponent implements OnInit, ControlValueAcc
currentSecurityMode = null;
bootstrapDisabled = false;
shortServerIdMin = 1;
shortServerIdMax = 65534;
readonly shortServerIdMin = 1;
readonly shortServerIdMax = 65534;
@Input()
@coerceBoolean()
@ -94,18 +94,15 @@ export class Lwm2mDeviceConfigServerComponent implements OnInit, ControlValueAcc
}
ngOnInit(): void {
if (this.isBootstrap) {
this.shortServerIdMin = 0;
this.shortServerIdMax = 65535;
}
this.serverFormGroup = this.fb.group({
host: ['', Validators.required],
port: ['', [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern('[0-9]*')]],
securityMode: [Lwm2mSecurityType.NO_SEC],
serverPublicKey: [''],
clientHoldOffTime: ['', [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
shortServerId: ['',
[Validators.required, Validators.min(this.shortServerIdMin), Validators.max(this.shortServerIdMax), Validators.pattern('[0-9]*')]],
shortServerId: ['', this.isBootstrap ?
[] : [Validators.required, Validators.pattern('[0-9]*'), Validators.min(this.shortServerIdMin), Validators.max(this.shortServerIdMax)]
],
bootstrapServerAccountTimeout: ['', [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
binding: [''],
lifetime: [null, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
@ -129,6 +126,7 @@ export class Lwm2mDeviceConfigServerComponent implements OnInit, ControlValueAcc
this.serverFormGroup.get('serverPublicKey').patchValue(serverSecurityConfig.serverCertificate, {emitEvent: false});
}
});
this.serverFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(value => {

3
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1833,6 +1833,8 @@
"bootstrap-tab": "Bootstrap Client",
"bootstrap-server": "Bootstrap Server",
"lwm2m-server": "LwM2M Server",
"client-reboot": "Registration Update Trigger",
"bootstrap-reboot": "Bootstrap-Request Trigger",
"client-publicKey-or-id": "Client Public Key or Id",
"client-publicKey-or-id-required": "Client Public Key or Id is required.",
"client-publicKey-or-id-tooltip-psk": "The PSK identifier is an arbitrary PSK identifier up to 128 bytes, as described in the standard [RFC7925].\nThe PSK identifier MUST first be converted to a character string and then encoded into octets using UTF-8.",
@ -2334,6 +2336,7 @@
"short-id-required": "Short server ID is required.",
"short-id-range": "Short server ID should be in a range from {{ min }} to {{ max }}.",
"short-id-pattern": "Short server ID must be a positive integer.",
"short-id-pattern-bs": "Short server ID must be only null",
"lifetime": "Client registration lifetime",
"lifetime-required": "Client registration lifetime is required.",
"lifetime-pattern": "Client registration lifetime must be a positive integer.",

Loading…
Cancel
Save