diff --git a/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java b/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java index 0855e4ee1f..9e6d393b30 100644 --- a/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java +++ b/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java @@ -17,6 +17,7 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.SecurityMode; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -46,9 +47,11 @@ public class Lwm2mController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/lwm2m/deviceProfile/bootstrap/{securityMode}/{bootstrapServerIs}", method = RequestMethod.GET) @ResponseBody - public ServerSecurityConfig getLwm2mBootstrapSecurityInfo(@PathVariable("securityMode") String securityMode, + public ServerSecurityConfig getLwm2mBootstrapSecurityInfo(@PathVariable("securityMode") String strSecurityMode, @PathVariable("bootstrapServerIs") boolean bootstrapServer) throws ThingsboardException { + checkNotNull(strSecurityMode); try { + SecurityMode securityMode = SecurityMode.valueOf(strSecurityMode); return lwM2MServerSecurityInfoRepository.getServerSecurityInfo(securityMode, bootstrapServer); } catch (Exception e) { throw handleException(e); diff --git a/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServerSecurityInfoRepository.java b/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServerSecurityInfoRepository.java index 012cd3e359..06190cdf70 100644 --- a/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServerSecurityInfoRepository.java +++ b/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServerSecurityInfoRepository.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.lwm2m; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.SecurityMode; import org.eclipse.leshan.core.util.Hex; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; @@ -25,7 +26,6 @@ import org.thingsboard.server.common.data.lwm2m.ServerSecurityConfig; import org.thingsboard.server.transport.lwm2m.config.LwM2MSecureServerConfig; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportBootstrapConfig; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; -import org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode; import java.math.BigInteger; import java.security.AlgorithmParameters; @@ -55,17 +55,16 @@ public class LwM2MServerSecurityInfoRepository { * @param bootstrapServer * @return ServerSecurityConfig more value is default: Important - port, host, publicKey */ - public ServerSecurityConfig getServerSecurityInfo(String securityMode, boolean bootstrapServer) { - LwM2MSecurityMode lwM2MSecurityMode = LwM2MSecurityMode.fromSecurityMode(securityMode.toLowerCase()); - ServerSecurityConfig result = getServerSecurityConfig(bootstrapServer ? bootstrapConfig : serverConfig, lwM2MSecurityMode); + public ServerSecurityConfig getServerSecurityInfo(SecurityMode securityMode, boolean bootstrapServer) { + ServerSecurityConfig result = getServerSecurityConfig(bootstrapServer ? bootstrapConfig : serverConfig, securityMode); result.setBootstrapServerIs(bootstrapServer); return result; } - private ServerSecurityConfig getServerSecurityConfig(LwM2MSecureServerConfig serverConfig, LwM2MSecurityMode mode) { + private ServerSecurityConfig getServerSecurityConfig(LwM2MSecureServerConfig serverConfig, SecurityMode securityMode) { ServerSecurityConfig bsServ = new ServerSecurityConfig(); bsServ.setServerId(serverConfig.getId()); - switch (mode) { + switch (securityMode) { case NO_SEC: bsServ.setHost(serverConfig.getHost()); bsServ.setPort(serverConfig.getPort()); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 1cc89feab2..7fde122cf6 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -643,6 +643,7 @@ transport: private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED:308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420dc774b309e547ceb48fee547e104ce201a9c48c449dc5414cd04e7f5cf05f67ba00a06082a8648ce3d030107a1440342000405064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f3585eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b}" # Only Certificate_x509: alias: "${LWM2M_KEYSTORE_ALIAS_SERVER:server}" + skip_validity_check_for_client_cert: "${TB_LWM2M_SERVER_SECURITY_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" bootstrap: enable: "${LWM2M_ENABLED_BS:true}" id: "${LWM2M_SERVER_ID_BS:111}" diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 2329e8086c..937244fc93 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -22,6 +22,7 @@ import io.jsonwebtoken.Claims; import io.jsonwebtoken.Header; import io.jsonwebtoken.Jwt; import io.jsonwebtoken.Jwts; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; @@ -120,7 +121,7 @@ public abstract class AbstractWebTest { protected String refreshToken; protected String username; - private TenantId tenantId; + protected TenantId tenantId; @SuppressWarnings("rawtypes") private HttpMessageConverter mappingJackson2HttpMessageConverter; diff --git a/application/src/test/java/org/thingsboard/server/transport/TransportSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/transport/TransportSqlTestSuite.java index d16bbc3885..25df3bee00 100644 --- a/application/src/test/java/org/thingsboard/server/transport/TransportSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/transport/TransportSqlTestSuite.java @@ -32,7 +32,8 @@ import java.util.Arrays; "org.thingsboard.server.transport.*.attributes.updates.sql.*Test", "org.thingsboard.server.transport.*.attributes.request.sql.*Test", "org.thingsboard.server.transport.*.claim.sql.*Test", - "org.thingsboard.server.transport.*.provision.sql.*Test" + "org.thingsboard.server.transport.*.provision.sql.*Test", + "org.thingsboard.server.transport.lwm2m.*Test" }) public class TransportSqlTestSuite { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java new file mode 100644 index 0000000000..90b926e78a --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -0,0 +1,222 @@ +/** + * Copyright © 2016-2021 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; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.commons.io.IOUtils; +import org.eclipse.leshan.core.util.Hex; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration; +import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; +import org.thingsboard.server.controller.AbstractWebsocketTest; +import org.thingsboard.server.controller.TbTestWebSocketClient; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.security.AlgorithmParameters; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.KeySpec; +import java.util.Base64; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +@DaoSqlTest +public class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest { + + protected DeviceProfile deviceProfile; + protected ScheduledExecutorService executor; + protected TbTestWebSocketClient wsClient; + + protected final PublicKey clientPublicKey; // client public key used for RPK + protected final PrivateKey clientPrivateKey; // client private key used for RPK + protected final PublicKey serverPublicKey; // server public key used for RPK + protected final PrivateKey serverPrivateKey; // server private key used for RPK + + // client private key used for X509 + protected final PrivateKey clientPrivateKeyFromCert; + // server private key used for X509 + protected final PrivateKey serverPrivateKeyFromCert; + // client certificate signed by rootCA with a good CN (CN start by leshan_integration_test) + protected final X509Certificate clientX509Cert; + // client certificate signed by rootCA but with bad CN (CN does not start by leshan_integration_test) + protected final X509Certificate clientX509CertWithBadCN; + // client certificate self-signed with a good CN (CN start by leshan_integration_test) + protected final X509Certificate clientX509CertSelfSigned; + // client certificate signed by another CA (not rootCA) with a good CN (CN start by leshan_integration_test) + protected final X509Certificate clientX509CertNotTrusted; + // server certificate signed by rootCA + protected final X509Certificate serverX509Cert; + // self-signed server certificate + protected final X509Certificate serverX509CertSelfSigned; + // rootCA used by the server + protected final X509Certificate rootCAX509Cert; + // certificates trustedby the server (should contain rootCA) + protected final Certificate[] trustedCertificates = new Certificate[1]; + + public AbstractLwM2MIntegrationTest() { +// create client credentials + try { + // Get point values + byte[] publicX = Hex + .decodeHex("89c048261979208666f2bfb188be1968fc9021c416ce12828c06f4e314c167b5".toCharArray()); + byte[] publicY = Hex + .decodeHex("cbf1eb7587f08e01688d9ada4be859137ca49f79394bad9179326b3090967b68".toCharArray()); + byte[] privateS = Hex + .decodeHex("e67b68d2aaeb6550f19d98cade3ad62b39532e02e6b422e1f7ea189dabaea5d2".toCharArray()); + + // Get Elliptic Curve Parameter spec for secp256r1 + AlgorithmParameters algoParameters = AlgorithmParameters.getInstance("EC"); + algoParameters.init(new ECGenParameterSpec("secp256r1")); + ECParameterSpec parameterSpec = algoParameters.getParameterSpec(ECParameterSpec.class); + + // Create key specs + KeySpec publicKeySpec = new ECPublicKeySpec(new ECPoint(new BigInteger(publicX), new BigInteger(publicY)), + parameterSpec); + KeySpec privateKeySpec = new ECPrivateKeySpec(new BigInteger(privateS), parameterSpec); + + // Get keys + clientPublicKey = KeyFactory.getInstance("EC").generatePublic(publicKeySpec); + clientPrivateKey = KeyFactory.getInstance("EC").generatePrivate(privateKeySpec); + + // Get certificates from key store + char[] clientKeyStorePwd = "client".toCharArray(); + KeyStore clientKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + try (InputStream clientKeyStoreFile = this.getClass().getClassLoader().getResourceAsStream("lwm2m/credentials/clientKeyStore.jks")) { + clientKeyStore.load(clientKeyStoreFile, clientKeyStorePwd); + } + + clientPrivateKeyFromCert = (PrivateKey) clientKeyStore.getKey("client", clientKeyStorePwd); + clientX509Cert = (X509Certificate) clientKeyStore.getCertificate("client"); + clientX509CertWithBadCN = (X509Certificate) clientKeyStore.getCertificate("client_bad_cn"); + clientX509CertSelfSigned = (X509Certificate) clientKeyStore.getCertificate("client_self_signed"); + clientX509CertNotTrusted = (X509Certificate) clientKeyStore.getCertificate("client_not_trusted"); + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException(e); + } + + // create server credentials + try { + // Get point values + byte[] publicX = Hex + .decodeHex("fcc28728c123b155be410fc1c0651da374fc6ebe7f96606e90d927d188894a73".toCharArray()); + byte[] publicY = Hex + .decodeHex("d2ffaa73957d76984633fc1cc54d0b763ca0559a9dff9706e9f4557dacc3f52a".toCharArray()); + byte[] privateS = Hex + .decodeHex("1dae121ba406802ef07c193c1ee4df91115aabd79c1ed7f4c0ef7ef6a5449400".toCharArray()); + + // Get Elliptic Curve Parameter spec for secp256r1 + AlgorithmParameters algoParameters = AlgorithmParameters.getInstance("EC"); + algoParameters.init(new ECGenParameterSpec("secp256r1")); + ECParameterSpec parameterSpec = algoParameters.getParameterSpec(ECParameterSpec.class); + + // Create key specs + KeySpec publicKeySpec = new ECPublicKeySpec(new ECPoint(new BigInteger(publicX), new BigInteger(publicY)), + parameterSpec); + KeySpec privateKeySpec = new ECPrivateKeySpec(new BigInteger(privateS), parameterSpec); + + // Get keys + serverPublicKey = KeyFactory.getInstance("EC").generatePublic(publicKeySpec); + serverPrivateKey = KeyFactory.getInstance("EC").generatePrivate(privateKeySpec); + + // Get certificates from key store + char[] serverKeyStorePwd = "server".toCharArray(); + KeyStore serverKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + try (InputStream serverKeyStoreFile = this.getClass().getClassLoader().getResourceAsStream("lwm2m/credentials/serverKeyStore.jks")) { + serverKeyStore.load(serverKeyStoreFile, serverKeyStorePwd); + } + + serverPrivateKeyFromCert = (PrivateKey) serverKeyStore.getKey("server", serverKeyStorePwd); + rootCAX509Cert = (X509Certificate) serverKeyStore.getCertificate("rootCA"); + serverX509Cert = (X509Certificate) serverKeyStore.getCertificate("server"); + serverX509CertSelfSigned = (X509Certificate) serverKeyStore.getCertificate("server_self_signed"); + trustedCertificates[0] = rootCAX509Cert; + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException(e); + } + } + + @Before + public void beforeTest() throws Exception { + executor = Executors.newScheduledThreadPool(10); + loginTenantAdmin(); + + String[] resources = new String[]{"1.xml", "2.xml", "3.xml"}; + for (String resourceName : resources) { + TbResource lwModel = new TbResource(); + lwModel.setResourceType(ResourceType.LWM2M_MODEL); + lwModel.setTitle(resourceName); + lwModel.setFileName(resourceName); + lwModel.setTenantId(tenantId); + byte[] bytes = IOUtils.toByteArray(AbstractLwM2MIntegrationTest.class.getClassLoader().getResourceAsStream("lwm2m/" + resourceName)); + lwModel.setData(Base64.getEncoder().encodeToString(bytes)); + lwModel = doPostWithTypedResponse("/api/resource", lwModel, new TypeReference<>() { + }); + Assert.assertNotNull(lwModel); + } + wsClient = buildAndConnectWebSocketClient(); + } + + protected void createDeviceProfile(String transportConfiguration) throws Exception { + deviceProfile = new DeviceProfile(); + + deviceProfile.setName("LwM2M"); + deviceProfile.setType(DeviceProfileType.DEFAULT); + deviceProfile.setTenantId(tenantId); + deviceProfile.setTransportType(DeviceTransportType.LWM2M); + deviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED); + deviceProfile.setDescription(deviceProfile.getName()); + + DeviceProfileData deviceProfileData = new DeviceProfileData(); + deviceProfileData.setConfiguration(new DefaultDeviceProfileConfiguration()); + deviceProfileData.setProvisionConfiguration(new DisabledDeviceProfileProvisionConfiguration(null)); + deviceProfileData.setTransportConfiguration(JacksonUtil.fromString(transportConfiguration, Lwm2mDeviceProfileTransportConfiguration.class)); + deviceProfile.setProfileData(deviceProfileData); + + deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + Assert.assertNotNull(deviceProfile); + } + + @After + public void after() { + executor.shutdownNow(); + wsClient.close(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/NoSecLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/NoSecLwM2MIntegrationTest.java new file mode 100644 index 0000000000..c82845f20c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/NoSecLwM2MIntegrationTest.java @@ -0,0 +1,163 @@ +/** + * Copyright © 2016-2021 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; + +import org.eclipse.californium.core.network.config.NetworkConfig; +import org.eclipse.leshan.client.object.Security; +import org.jetbrains.annotations.NotNull; +import org.junit.Assert; +import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.SingleEntityFilter; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; +import org.thingsboard.server.service.telemetry.cmd.v2.LatestValueCmd; +import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient; +import org.thingsboard.server.transport.lwm2m.secure.credentials.LwM2MCredentials; +import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCredentials; + +import java.util.Collections; +import java.util.List; + +import static org.eclipse.leshan.client.object.Security.noSec; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { + + protected final String TRANSPORT_CONFIGURATION = "{\n" + + " \"type\": \"LWM2M\",\n" + + " \"observeAttr\": {\n" + + " \"keyName\": {\n" + + " \"/3_1.0/0/9\": \"batteryLevel\"\n" + + " },\n" + + " \"observe\": [],\n" + + " \"attribute\": [\n" + + " ],\n" + + " \"telemetry\": [\n" + + " \"/3_1.0/0/9\"\n" + + " ],\n" + + " \"attributeLwm2m\": {}\n" + + " },\n" + + " \"bootstrap\": {\n" + + " \"servers\": {\n" + + " \"binding\": \"UQ\",\n" + + " \"shortId\": 123,\n" + + " \"lifetime\": 300,\n" + + " \"notifIfDisabled\": true,\n" + + " \"defaultMinPeriod\": 1\n" + + " },\n" + + " \"lwm2mServer\": {\n" + + " \"host\": \"localhost\",\n" + + " \"port\": 5685,\n" + + " \"serverId\": 123,\n" + + " \"securityMode\": \"NO_SEC\",\n" + + " \"serverPublicKey\": \"\",\n" + + " \"bootstrapServerIs\": false,\n" + + " \"clientHoldOffTime\": 1,\n" + + " \"bootstrapServerAccountTimeout\": 0\n" + + " },\n" + + " \"bootstrapServer\": {\n" + + " \"host\": \"localhost\",\n" + + " \"port\": 5687,\n" + + " \"serverId\": 111,\n" + + " \"securityMode\": \"NO_SEC\",\n" + + " \"serverPublicKey\": \"\",\n" + + " \"bootstrapServerIs\": true,\n" + + " \"clientHoldOffTime\": 1,\n" + + " \"bootstrapServerAccountTimeout\": 0\n" + + " }\n" + + " },\n" + + " \"clientLwM2mSettings\": {\n" + + " \"clientOnlyObserveAfterConnect\": 1\n" + + " }\n" + + "}"; + + private final int port = 5685; + private final Security security = noSec("coap://localhost:" + port, 123); + private final NetworkConfig coapConfig = new NetworkConfig().setString("COAP_PORT", Integer.toString(port)); + + @NotNull + private Device createDevice(String deviceAEndpoint) throws Exception { + Device device = new Device(); + device.setName("Device A"); + device.setDeviceProfileId(deviceProfile.getId()); + device.setTenantId(tenantId); + device = doPost("/api/device", device, Device.class); + Assert.assertNotNull(device); + + DeviceCredentials deviceCredentials = + doGet("/api/device/" + device.getId().getId().toString() + "/credentials", DeviceCredentials.class); + Assert.assertEquals(device.getId(), deviceCredentials.getDeviceId()); + deviceCredentials.setCredentialsType(DeviceCredentialsType.LWM2M_CREDENTIALS); + + LwM2MCredentials noSecCredentials = new LwM2MCredentials(); + NoSecClientCredentials clientCredentials = new NoSecClientCredentials(); + clientCredentials.setEndpoint(deviceAEndpoint); + noSecCredentials.setClient(clientCredentials); + deviceCredentials.setCredentialsValue(JacksonUtil.toString(noSecCredentials)); + doPost("/api/device/credentials", deviceCredentials).andExpect(status().isOk()); + return device; + } + + @Test + public void testConnectAndObserveTelemetry() throws Exception { + createDeviceProfile(TRANSPORT_CONFIGURATION); + + String deviceAEndpoint = "deviceAEndpoint"; + + Device device = createDevice(deviceAEndpoint); + + SingleEntityFilter sef = new SingleEntityFilter(); + sef.setSingleEntity(device.getId()); + LatestValueCmd latestCmd = new LatestValueCmd(); + latestCmd.setKeys(Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, "batteryLevel"))); + EntityDataQuery edq = new EntityDataQuery(sef, new EntityDataPageLink(1, 0, null, null), + Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + + EntityDataCmd cmd = new EntityDataCmd(1, edq, null, latestCmd, null); + TelemetryPluginCmdsWrapper wrapper = new TelemetryPluginCmdsWrapper(); + wrapper.setEntityDataCmds(Collections.singletonList(cmd)); + + wsClient.send(mapper.writeValueAsString(wrapper)); + wsClient.waitForReply(); + + wsClient.registerWaitForUpdate(); + LwM2MTestClient client = new LwM2MTestClient(executor, deviceAEndpoint); + client.init(security, coapConfig); + String msg = wsClient.waitForUpdate(); + + EntityDataUpdate update = mapper.readValue(msg, EntityDataUpdate.class); + Assert.assertEquals(1, update.getCmdId()); + List eData = update.getUpdate(); + Assert.assertNotNull(eData); + Assert.assertEquals(1, eData.size()); + Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); + Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES)); + var tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("batteryLevel"); + Assert.assertEquals(42, Long.parseLong(tsValue.getValue())); + client.destroy(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/X509LwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/X509LwM2MIntegrationTest.java new file mode 100644 index 0000000000..18749cfee5 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/X509LwM2MIntegrationTest.java @@ -0,0 +1,207 @@ +/** + * Copyright © 2016-2021 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; + +import org.eclipse.californium.core.network.config.NetworkConfig; +import org.eclipse.leshan.client.object.Security; +import org.jetbrains.annotations.NotNull; +import org.junit.Assert; +import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ClientCredentials; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.SingleEntityFilter; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.transport.util.SslUtil; +import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; +import org.thingsboard.server.service.telemetry.cmd.v2.LatestValueCmd; +import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient; +import org.thingsboard.server.transport.lwm2m.secure.credentials.LwM2MCredentials; + +import java.util.Collections; +import java.util.List; + +import static org.eclipse.leshan.client.object.Security.x509; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class X509LwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { + + protected final String TRANSPORT_CONFIGURATION = "{\n" + + " \"type\": \"LWM2M\",\n" + + " \"observeAttr\": {\n" + + " \"keyName\": {\n" + + " \"/3_1.0/0/9\": \"batteryLevel\"\n" + + " },\n" + + " \"observe\": [],\n" + + " \"attribute\": [\n" + + " ],\n" + + " \"telemetry\": [\n" + + " \"/3_1.0/0/9\"\n" + + " ],\n" + + " \"attributeLwm2m\": {}\n" + + " },\n" + + " \"bootstrap\": {\n" + + " \"servers\": {\n" + + " \"binding\": \"UQ\",\n" + + " \"shortId\": 123,\n" + + " \"lifetime\": 300,\n" + + " \"notifIfDisabled\": true,\n" + + " \"defaultMinPeriod\": 1\n" + + " },\n" + + " \"lwm2mServer\": {\n" + + " \"host\": \"localhost\",\n" + + " \"port\": 5686,\n" + + " \"serverId\": 123,\n" + + " \"serverPublicKey\": \"\",\n" + + " \"bootstrapServerIs\": false,\n" + + " \"clientHoldOffTime\": 1,\n" + + " \"bootstrapServerAccountTimeout\": 0\n" + + " },\n" + + " \"bootstrapServer\": {\n" + + " \"host\": \"localhost\",\n" + + " \"port\": 5687,\n" + + " \"serverId\": 111,\n" + + " \"securityMode\": \"NO_SEC\",\n" + + " \"serverPublicKey\": \"\",\n" + + " \"bootstrapServerIs\": true,\n" + + " \"clientHoldOffTime\": 1,\n" + + " \"bootstrapServerAccountTimeout\": 0\n" + + " }\n" + + " },\n" + + " \"clientLwM2mSettings\": {\n" + + " \"clientOnlyObserveAfterConnect\": 1\n" + + " }\n" + + "}"; + + + private final int port = 5686; + private final NetworkConfig coapConfig = new NetworkConfig().setString("COAP_SECURE_PORT", Integer.toString(port)); + private final String endpoint = "deviceAEndpoint"; + private final String serverUri = "coaps://localhost:" + port; + + @NotNull + private Device createDevice(X509ClientCredentials clientCredentials) throws Exception { + Device device = new Device(); + device.setName("Device A"); + device.setDeviceProfileId(deviceProfile.getId()); + device.setTenantId(tenantId); + device = doPost("/api/device", device, Device.class); + Assert.assertNotNull(device); + + DeviceCredentials deviceCredentials = + doGet("/api/device/" + device.getId().getId().toString() + "/credentials", DeviceCredentials.class); + Assert.assertEquals(device.getId(), deviceCredentials.getDeviceId()); + deviceCredentials.setCredentialsType(DeviceCredentialsType.LWM2M_CREDENTIALS); + + LwM2MCredentials credentials = new LwM2MCredentials(); + + credentials.setClient(clientCredentials); + + deviceCredentials.setCredentialsValue(JacksonUtil.toString(credentials)); + doPost("/api/device/credentials", deviceCredentials).andExpect(status().isOk()); + return device; + } + + @Test + public void testConnectAndObserveTelemetry() throws Exception { + createDeviceProfile(TRANSPORT_CONFIGURATION); + X509ClientCredentials credentials = new X509ClientCredentials(); + credentials.setEndpoint(endpoint); + Device device = createDevice(credentials); + + SingleEntityFilter sef = new SingleEntityFilter(); + sef.setSingleEntity(device.getId()); + LatestValueCmd latestCmd = new LatestValueCmd(); + latestCmd.setKeys(Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, "batteryLevel"))); + EntityDataQuery edq = new EntityDataQuery(sef, new EntityDataPageLink(1, 0, null, null), + Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + + EntityDataCmd cmd = new EntityDataCmd(1, edq, null, latestCmd, null); + TelemetryPluginCmdsWrapper wrapper = new TelemetryPluginCmdsWrapper(); + wrapper.setEntityDataCmds(Collections.singletonList(cmd)); + + wsClient.send(mapper.writeValueAsString(wrapper)); + wsClient.waitForReply(); + + wsClient.registerWaitForUpdate(); + LwM2MTestClient client = new LwM2MTestClient(executor, endpoint); + Security security = x509(serverUri, 123, clientX509Cert.getEncoded(), clientPrivateKeyFromCert.getEncoded(), serverX509Cert.getEncoded()); + client.init(security, coapConfig); + String msg = wsClient.waitForUpdate(); + + EntityDataUpdate update = mapper.readValue(msg, EntityDataUpdate.class); + Assert.assertEquals(1, update.getCmdId()); + List eData = update.getUpdate(); + Assert.assertNotNull(eData); + Assert.assertEquals(1, eData.size()); + Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); + Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES)); + var tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("batteryLevel"); + Assert.assertEquals(42, Long.parseLong(tsValue.getValue())); + client.destroy(); + } + + @Test + public void testConnectWithCertAndObserveTelemetry() throws Exception { + createDeviceProfile(TRANSPORT_CONFIGURATION); + X509ClientCredentials credentials = new X509ClientCredentials(); + credentials.setEndpoint(endpoint); + credentials.setCert(SslUtil.getCertificateString(clientX509CertNotTrusted)); + Device device = createDevice(credentials); + + SingleEntityFilter sef = new SingleEntityFilter(); + sef.setSingleEntity(device.getId()); + LatestValueCmd latestCmd = new LatestValueCmd(); + latestCmd.setKeys(Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, "batteryLevel"))); + EntityDataQuery edq = new EntityDataQuery(sef, new EntityDataPageLink(1, 0, null, null), + Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + + EntityDataCmd cmd = new EntityDataCmd(1, edq, null, latestCmd, null); + TelemetryPluginCmdsWrapper wrapper = new TelemetryPluginCmdsWrapper(); + wrapper.setEntityDataCmds(Collections.singletonList(cmd)); + + wsClient.send(mapper.writeValueAsString(wrapper)); + wsClient.waitForReply(); + + wsClient.registerWaitForUpdate(); + LwM2MTestClient client = new LwM2MTestClient(executor, endpoint); + + Security security = x509(serverUri, 123, clientX509CertNotTrusted.getEncoded(), clientPrivateKeyFromCert.getEncoded(), serverX509Cert.getEncoded()); + + client.init(security, coapConfig); + String msg = wsClient.waitForUpdate(); + + EntityDataUpdate update = mapper.readValue(msg, EntityDataUpdate.class); + Assert.assertEquals(1, update.getCmdId()); + List eData = update.getUpdate(); + Assert.assertNotNull(eData); + Assert.assertEquals(1, eData.size()); + Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); + Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES)); + var tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("batteryLevel"); + Assert.assertEquals(42, Long.parseLong(tsValue.getValue())); + client.destroy(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java new file mode 100644 index 0000000000..8a17b6e3c9 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java @@ -0,0 +1,259 @@ +/** + * Copyright © 2016-2021 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.client; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.network.config.NetworkConfig; +import org.eclipse.californium.elements.Connector; +import org.eclipse.californium.scandium.DTLSConnector; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.eclipse.californium.scandium.dtls.ClientHandshaker; +import org.eclipse.californium.scandium.dtls.DTLSSession; +import org.eclipse.californium.scandium.dtls.HandshakeException; +import org.eclipse.californium.scandium.dtls.Handshaker; +import org.eclipse.californium.scandium.dtls.ResumingClientHandshaker; +import org.eclipse.californium.scandium.dtls.ResumingServerHandshaker; +import org.eclipse.californium.scandium.dtls.ServerHandshaker; +import org.eclipse.californium.scandium.dtls.SessionAdapter; +import org.eclipse.leshan.client.californium.LeshanClient; +import org.eclipse.leshan.client.californium.LeshanClientBuilder; +import org.eclipse.leshan.client.engine.DefaultRegistrationEngineFactory; +import org.eclipse.leshan.client.object.Security; +import org.eclipse.leshan.client.object.Server; +import org.eclipse.leshan.client.observer.LwM2mClientObserver; +import org.eclipse.leshan.client.resource.ObjectsInitializer; +import org.eclipse.leshan.client.servers.ServerIdentity; +import org.eclipse.leshan.core.ResponseCode; +import org.eclipse.leshan.core.californium.DefaultEndpointFactory; +import org.eclipse.leshan.core.model.LwM2mModel; +import org.eclipse.leshan.core.model.ObjectLoader; +import org.eclipse.leshan.core.model.ObjectModel; +import org.eclipse.leshan.core.model.StaticModel; +import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeDecoder; +import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeEncoder; +import org.eclipse.leshan.core.request.BindingMode; +import org.eclipse.leshan.core.request.BootstrapRequest; +import org.eclipse.leshan.core.request.DeregisterRequest; +import org.eclipse.leshan.core.request.RegisterRequest; +import org.eclipse.leshan.core.request.UpdateRequest; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; + +import static org.eclipse.leshan.core.LwM2mId.DEVICE; +import static org.eclipse.leshan.core.LwM2mId.SECURITY; +import static org.eclipse.leshan.core.LwM2mId.SERVER; + +@Slf4j +@Data +public class LwM2MTestClient { + + private final ScheduledExecutorService executor; + private final String endpoint; + private LeshanClient client; + + public void init(Security security, NetworkConfig coapConfig) { + String[] resources = new String[]{"0.xml", "1.xml", "2.xml", "3.xml"}; + List models = new ArrayList<>(); + for (String resourceName : resources) { + models.addAll(ObjectLoader.loadDdfFile(LwM2MTestClient.class.getClassLoader().getResourceAsStream("lwm2m/" + resourceName), resourceName)); + } + LwM2mModel model = new StaticModel(models); + ObjectsInitializer initializer = new ObjectsInitializer(model); + initializer.setInstancesForObject(SECURITY, security); + initializer.setInstancesForObject(SERVER, new Server(123, 300, BindingMode.U, false)); + initializer.setInstancesForObject(DEVICE, new SimpleLwM2MDevice()); + + DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder(); + dtlsConfig.setRecommendedCipherSuitesOnly(true); + + DefaultRegistrationEngineFactory engineFactory = new DefaultRegistrationEngineFactory(); + engineFactory.setReconnectOnUpdate(false); + engineFactory.setResumeOnConnect(true); + + DefaultEndpointFactory endpointFactory = new DefaultEndpointFactory(endpoint) { + @Override + protected Connector createSecuredConnector(DtlsConnectorConfig dtlsConfig) { + + return new DTLSConnector(dtlsConfig) { + @Override + protected void onInitializeHandshaker(Handshaker handshaker) { + handshaker.addSessionListener(new SessionAdapter() { + + @Override + public void handshakeStarted(Handshaker handshaker) throws HandshakeException { + if (handshaker instanceof ServerHandshaker) { + log.info("DTLS Full Handshake initiated by server : STARTED ..."); + } else if (handshaker instanceof ResumingServerHandshaker) { + log.info("DTLS abbreviated Handshake initiated by server : STARTED ..."); + } else if (handshaker instanceof ClientHandshaker) { + log.info("DTLS Full Handshake initiated by client : STARTED ..."); + } else if (handshaker instanceof ResumingClientHandshaker) { + log.info("DTLS abbreviated Handshake initiated by client : STARTED ..."); + } + } + + @Override + public void sessionEstablished(Handshaker handshaker, DTLSSession establishedSession) + throws HandshakeException { + if (handshaker instanceof ServerHandshaker) { + log.info("DTLS Full Handshake initiated by server : SUCCEED, handshaker {}", handshaker); + } else if (handshaker instanceof ResumingServerHandshaker) { + log.info("DTLS abbreviated Handshake initiated by server : SUCCEED, handshaker {}", handshaker); + } else if (handshaker instanceof ClientHandshaker) { + log.info("DTLS Full Handshake initiated by client : SUCCEED, handshaker {}", handshaker); + } else if (handshaker instanceof ResumingClientHandshaker) { + log.info("DTLS abbreviated Handshake initiated by client : SUCCEED, handshaker {}", handshaker); + } + } + + @Override + public void handshakeFailed(Handshaker handshaker, Throwable error) { + /** get cause */ + String cause; + if (error != null) { + if (error.getMessage() != null) { + cause = error.getMessage(); + } else { + cause = error.getClass().getName(); + } + } else { + cause = "unknown cause"; + } + + if (handshaker instanceof ServerHandshaker) { + log.info("DTLS Full Handshake initiated by server : FAILED [{}]", cause); + } else if (handshaker instanceof ResumingServerHandshaker) { + log.info("DTLS abbreviated Handshake initiated by server : FAILED [{}]", cause); + } else if (handshaker instanceof ClientHandshaker) { + log.info("DTLS Full Handshake initiated by client : FAILED [{}]", cause); + } else if (handshaker instanceof ResumingClientHandshaker) { + log.info("DTLS abbreviated Handshake initiated by client : FAILED [{}]", cause); + } + } + }); + } + }; + } + }; + + LeshanClientBuilder builder = new LeshanClientBuilder(endpoint); + builder.setLocalAddress("0.0.0.0", 11000); + builder.setObjects(initializer.createAll()); + builder.setCoapConfig(coapConfig); + builder.setDtlsConfig(dtlsConfig); + builder.setRegistrationEngineFactory(engineFactory); + builder.setEndpointFactory(endpointFactory); + builder.setSharedExecutor(executor); + builder.setDecoder(new DefaultLwM2mNodeDecoder(true)); + builder.setEncoder(new DefaultLwM2mNodeEncoder(true)); + client = builder.build(); + + LwM2mClientObserver observer = new LwM2mClientObserver() { + @Override + public void onBootstrapStarted(ServerIdentity bsserver, BootstrapRequest request) { + log.info("ClientObserver -> onBootstrapStarted..."); + } + + @Override + public void onBootstrapSuccess(ServerIdentity bsserver, BootstrapRequest request) { + log.info("ClientObserver -> onBootstrapSuccess..."); + } + + @Override + public void onBootstrapFailure(ServerIdentity bsserver, BootstrapRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + log.info("ClientObserver -> onBootstrapFailure..."); + } + + @Override + public void onBootstrapTimeout(ServerIdentity bsserver, BootstrapRequest request) { + log.info("ClientObserver -> onBootstrapTimeout..."); + } + + @Override + public void onRegistrationStarted(ServerIdentity server, RegisterRequest request) { +// log.info("ClientObserver -> onRegistrationStarted... EndpointName [{}]", request.getEndpointName()); + } + + @Override + public void onRegistrationSuccess(ServerIdentity server, RegisterRequest request, String registrationID) { + log.info("ClientObserver -> onRegistrationSuccess... EndpointName [{}] [{}]", request.getEndpointName(), registrationID); + } + + @Override + public void onRegistrationFailure(ServerIdentity server, RegisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + log.info("ClientObserver -> onRegistrationFailure... ServerIdentity [{}]", server); + } + + @Override + public void onRegistrationTimeout(ServerIdentity server, RegisterRequest request) { + log.info("ClientObserver -> onRegistrationTimeout... RegisterRequest [{}]", request); + } + + @Override + public void onUpdateStarted(ServerIdentity server, UpdateRequest request) { +// log.info("ClientObserver -> onUpdateStarted... UpdateRequest [{}]", request); + } + + @Override + public void onUpdateSuccess(ServerIdentity server, UpdateRequest request) { +// log.info("ClientObserver -> onUpdateSuccess... UpdateRequest [{}]", request); + } + + @Override + public void onUpdateFailure(ServerIdentity server, UpdateRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + + } + + @Override + public void onUpdateTimeout(ServerIdentity server, UpdateRequest request) { + + } + + @Override + public void onDeregistrationStarted(ServerIdentity server, DeregisterRequest request) { + log.info("ClientObserver ->onDeregistrationStarted... DeregisterRequest [{}]", request.getRegistrationId()); + + } + + @Override + public void onDeregistrationSuccess(ServerIdentity server, DeregisterRequest request) { + log.info("ClientObserver ->onDeregistrationSuccess... DeregisterRequest [{}]", request.getRegistrationId()); + + } + + @Override + public void onDeregistrationFailure(ServerIdentity server, DeregisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + log.info("ClientObserver ->onDeregistrationFailure... DeregisterRequest [{}] [{}]", request.getRegistrationId(), request.getRegistrationId()); + } + + @Override + public void onDeregistrationTimeout(ServerIdentity server, DeregisterRequest request) { + log.info("ClientObserver ->onDeregistrationTimeout... DeregisterRequest [{}] [{}]", request.getRegistrationId(), request.getRegistrationId()); + } + }; + this.client.addObserver(observer); + + client.start(); + } + + public void destroy() { + client.destroy(true); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java new file mode 100644 index 0000000000..4512a94a27 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java @@ -0,0 +1,199 @@ +/** + * Copyright © 2016-2021 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.client; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.client.resource.BaseInstanceEnabler; +import org.eclipse.leshan.client.servers.ServerIdentity; +import org.eclipse.leshan.core.model.ObjectModel; +import org.eclipse.leshan.core.model.ResourceModel; +import org.eclipse.leshan.core.node.LwM2mResource; +import org.eclipse.leshan.core.response.ExecuteResponse; +import org.eclipse.leshan.core.response.ReadResponse; +import org.eclipse.leshan.core.response.WriteResponse; + +import javax.security.auth.Destroyable; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.TimeZone; + +@Slf4j +public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyable { + + + private static final Random RANDOM = new Random(); + private static final List supportedResources = Arrays.asList(0, 1, 2, 3 +// , 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21 + ); + + @Override + public ReadResponse read(ServerIdentity identity, int resourceid) { + if (!identity.isSystem()) + log.info("Read on Device resource /{}/{}/{}", getModel().id, getId(), resourceid); + switch (resourceid) { + case 0: + return ReadResponse.success(resourceid, getManufacturer()); + case 1: + return ReadResponse.success(resourceid, getModelNumber()); + case 2: + return ReadResponse.success(resourceid, getSerialNumber()); + case 3: + return ReadResponse.success(resourceid, getFirmwareVersion()); + case 9: + return ReadResponse.success(resourceid, getBatteryLevel()); + case 10: + return ReadResponse.success(resourceid, getMemoryFree()); + case 11: + Map errorCodes = new HashMap<>(); + errorCodes.put(0, getErrorCode()); + return ReadResponse.success(resourceid, errorCodes, ResourceModel.Type.INTEGER); + case 14: + return ReadResponse.success(resourceid, getUtcOffset()); + case 15: + return ReadResponse.success(resourceid, getTimezone()); + case 16: + return ReadResponse.success(resourceid, getSupportedBinding()); + case 17: + return ReadResponse.success(resourceid, getDeviceType()); + case 18: + return ReadResponse.success(resourceid, getHardwareVersion()); + case 19: + return ReadResponse.success(resourceid, getSoftwareVersion()); + case 20: + return ReadResponse.success(resourceid, getBatteryStatus()); + case 21: + return ReadResponse.success(resourceid, getMemoryTotal()); + default: + return super.read(identity, resourceid); + } + } + + @Override + public ExecuteResponse execute(ServerIdentity identity, int resourceid, String params) { + String withParams = null; + if (params != null && params.length() != 0) { + withParams = " with params " + params; + } + log.info("Execute on Device resource /{}/{}/{} {}", getModel().id, getId(), resourceid, withParams != null ? withParams : ""); + return ExecuteResponse.success(); + } + + @Override + public WriteResponse write(ServerIdentity identity, int resourceid, LwM2mResource value) { + log.info("Write on Device resource /{}/{}/{}", getModel().id, getId(), resourceid); + + switch (resourceid) { + case 13: + return WriteResponse.notFound(); + case 14: + setUtcOffset((String) value.getValue()); + fireResourcesChange(resourceid); + return WriteResponse.success(); + case 15: + setTimezone((String) value.getValue()); + fireResourcesChange(resourceid); + return WriteResponse.success(); + default: + return super.write(identity, resourceid, value); + } + } + + private String getManufacturer() { + return "Leshan Demo Device"; + } + + private String getModelNumber() { + return "Model 500"; + } + + private String getSerialNumber() { + return "LT-500-000-0001"; + } + + private String getFirmwareVersion() { + return "1.0.0"; + } + + private long getErrorCode() { + return 0; + } + + private int getBatteryLevel() { + return 42; + } + + private long getMemoryFree() { + return Runtime.getRuntime().freeMemory() / 1024; + } + + private String utcOffset = new SimpleDateFormat("X").format(Calendar.getInstance().getTime()); + + private String getUtcOffset() { + return utcOffset; + } + + private void setUtcOffset(String t) { + utcOffset = t; + } + + private String timeZone = TimeZone.getDefault().getID(); + + private String getTimezone() { + return timeZone; + } + + private void setTimezone(String t) { + timeZone = t; + } + + private String getSupportedBinding() { + return "U"; + } + + private String getDeviceType() { + return "Demo"; + } + + private String getHardwareVersion() { + return "1.0.1"; + } + + private String getSoftwareVersion() { + return "1.0.2"; + } + + private int getBatteryStatus() { + return RANDOM.nextInt(7); + } + + private long getMemoryTotal() { + return Runtime.getRuntime().totalMemory() / 1024; + } + + @Override + public List getAvailableResourceIds(ObjectModel model) { + return supportedResources; + } + + @Override + public void destroy() { + } +} diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties new file mode 100644 index 0000000000..cd9a981aed --- /dev/null +++ b/application/src/test/resources/application-test.properties @@ -0,0 +1,3 @@ +transport.lwm2m.security.key_store=lwm2m/credentials/serverKeyStore.jks +transport.lwm2m.security.key_store_password=server +edges.enabled=true \ No newline at end of file diff --git a/application/src/test/resources/logback.xml b/application/src/test/resources/logback.xml index f991a40078..81d213b42d 100644 --- a/application/src/test/resources/logback.xml +++ b/application/src/test/resources/logback.xml @@ -9,13 +9,15 @@ - + + - + + diff --git a/application/src/test/resources/lwm2m/0.xml b/application/src/test/resources/lwm2m/0.xml new file mode 100644 index 0000000000..81e8523880 --- /dev/null +++ b/application/src/test/resources/lwm2m/0.xml @@ -0,0 +1,405 @@ + + + + + + + LWM2M Security + + 0 + urn:oma:lwm2m:oma:0:1.2 + 1.1 + 1.2 + Multiple + Mandatory + + + LWM2M Server URI + + Single + Mandatory + String + 0..255 + + + + + Bootstrap-Server + + Single + Mandatory + Boolean + + + + + + Security Mode + + Single + Mandatory + Integer + 0..4 + + + + + Public Key or Identity + + Single + Mandatory + Opaque + + + + + + Server Public Key + + Single + Mandatory + Opaque + + + + + + Secret Key + + Single + Mandatory + Opaque + + + + + + SMS Security Mode + + Single + Optional + Integer + 0..255 + + + + + SMS Binding Key Parameters + + Single + Optional + Opaque + 6 + + + + + SMS Binding Secret Key(s) + + Single + Optional + Opaque + 16,32,48 + + + + + LwM2M Server SMS Number + + Single + Optional + String + + + + + + Short Server ID + + Single + Optional + Integer + 1..65534 + + + + + Client Hold Off Time + + Single + Optional + Integer + + s + + + + Bootstrap-Server Account Timeout + + Single + Optional + Integer + + s + + + + Matching Type + + Single + Optional + Integer + 0..3 + + + + + SNI + + Single + Optional + String + + + + + + Certificate Usage + + Single + Optional + Integer + 0..3 + + + + + DTLS/TLS Ciphersuite + + Multiple + Optional + Integer + + + + + OSCORE Security Mode + + Single + Optional + Objlnk + + + + + + Groups To Use by Client + + Multiple + Optional + Integer + 0..65535 + + + + + Signature Algorithms Supported by Server + + Multiple + Optional + Integer + 0..65535 + + + + Signature Algorithms To Use by Client + + Multiple + Optional + Integer + 0..65535 + + + + + Signature Algorithm Certs Supported by Server + + Multiple + Optional + Integer + 0..65535 + + + + + TLS 1.3 Features To Use by Client + + Single + Optional + Integer + 0..65535 + + + + + TLS Extensions Supported by Server + + Single + Optional + Integer + 0..65535 + + + + + TLS Extensions To Use by Client + + Single + Optional + Integer + 0..65535 + + + + + Secondary LwM2M Server URI + + Multiple + Optional + String + 0..255 + + + + MQTT Server + + Single + Optional + Objlnk + + + + + LwM2M COSE Security + + Multiple + Optional + Objlnk + + + + + RDS Destination Port + + Single + Optional + Integer + 0..15 + + + + RDS Source Port + + Single + Optional + Integer + 0..15 + + + + RDS Application ID + + Single + Optional + String + + + + + + + + diff --git a/application/src/test/resources/lwm2m/1.xml b/application/src/test/resources/lwm2m/1.xml new file mode 100644 index 0000000000..f31e839c96 --- /dev/null +++ b/application/src/test/resources/lwm2m/1.xml @@ -0,0 +1,360 @@ + + + + + + + LwM2M Server + + 1 + urn:oma:lwm2m:oma:1:1.2 + 1.2 + 1.2 + Multiple + Mandatory + + + Short Server ID + R + Single + Mandatory + Integer + 1..65534 + + + + + Lifetime + RW + Single + Mandatory + Integer + + s + + + + Default Minimum Period + RW + Single + Optional + Integer + + s + + + + Default Maximum Period + RW + Single + Optional + Integer + + s + + + + Disable + E + Single + Optional + + + + + + + Disable Timeout + RW + Single + Optional + Integer + + s + + + + Notification Storing When Disabled or Offline + RW + Single + Mandatory + Boolean + + + + + + Binding + RW + Single + Mandatory + String + + + + + + Registration Update Trigger + E + Single + Mandatory + + + + + + + Bootstrap-Request Trigger + E + Single + Optional + + + + + + + APN Link + RW + Single + Optional + Objlnk + + + + + + TLS-DTLS Alert Code + R + Single + Optional + Integer + 0..255 + + + + + Last Bootstrapped + R + Single + Optional + Time + + + + + + Registration Priority Order + R + Single + Optional + Integer + + + + + + Initial Registration Delay Timer + RW + Single + Optional + Integer + + s + + + + Registration Failure Block + R + Single + Optional + Boolean + + + + + + Bootstrap on Registration Failure + R + Single + Optional + Boolean + + + + + + Communication Retry Count + RW + Single + Optional + Integer + + + + + + Communication Retry Timer + RW + Single + Optional + Integer + + s + + + + Communication Sequence Delay Timer + RW + Single + Optional + Integer + + s + + + + Communication Sequence Retry Count + RW + Single + Optional + Integer + + + + + + Trigger + RW + Single + Optional + Boolean + + + + + + Preferred Transport + RW + Single + Optional + String + The possible values are those listed in the LwM2M Core Specification + + + + Mute Send + RW + Single + Optional + Boolean + + + + + + Alternate APN Links + RW + Multiple + Optional + Objlnk + + + + + + Supported Server Versions + RW + Multiple + Optional + String + + + + + + Default Notification Mode + RW + Single + Optional + Integer + 0..1 + + + + + Profile ID Hash Algorithm + RW + Single + Optional + Integer + 0..255 + + + + + + + diff --git a/application/src/test/resources/lwm2m/2.xml b/application/src/test/resources/lwm2m/2.xml new file mode 100644 index 0000000000..4ea5805b36 --- /dev/null +++ b/application/src/test/resources/lwm2m/2.xml @@ -0,0 +1,123 @@ + + + + + + + LwM2M Access Control + + 2 + urn:oma:lwm2m:oma:2:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Object ID + R + Single + Mandatory + Integer + 1..65534 + + + + + Object Instance ID + R + Single + Mandatory + Integer + 0..65535 + + + + + ACL + RW + Multiple + Optional + Integer + 0..31 + + + + + Access Control Owner + RW + Single + Mandatory + Integer + 0..65535 + + + + + + + diff --git a/application/src/test/resources/lwm2m/3.xml b/application/src/test/resources/lwm2m/3.xml new file mode 100644 index 0000000000..724fc4cb33 --- /dev/null +++ b/application/src/test/resources/lwm2m/3.xml @@ -0,0 +1,331 @@ + + + + + + + Device + + 3 + urn:oma:lwm2m:oma:3:1.0 + 1.1 + 1.0 + Single + Mandatory + + + Manufacturer + R + Single + Optional + String + + + + + + Model Number + R + Single + Optional + String + + + + + + Serial Number + R + Single + Optional + String + + + + + + Firmware Version + R + Single + Optional + String + + + + + + Reboot + E + Single + Mandatory + + + + + + + Factory Reset + E + Single + Optional + + + + + + + Available Power Sources + R + Multiple + Optional + Integer + 0..7 + + + + + Power Source Voltage + R + Multiple + Optional + Integer + + + + + + Power Source Current + R + Multiple + Optional + Integer + + + + + + Battery Level + R + Single + Optional + Integer + 0..100 + /100 + + + + Memory Free + R + Single + Optional + Integer + + + + + + Error Code + R + Multiple + Mandatory + Integer + 0..32 + + + + + Reset Error Code + E + Single + Optional + + + + + + + Current Time + RW + Single + Optional + Time + + + + + + UTC Offset + RW + Single + Optional + String + + + + + + Timezone + RW + Single + Optional + String + + + + + + Supported Binding and Modes + R + Single + Mandatory + String + + + + + Device Type + R + Single + Optional + String + + + + + Hardware Version + R + Single + Optional + String + + + + + Software Version + R + Single + Optional + String + + + + + Battery Status + R + Single + Optional + Integer + 0..6 + + + + Memory Total + R + Single + Optional + Integer + + + + + ExtDevInfo + R + Multiple + Optional + Objlnk + + + + + + + diff --git a/application/src/test/resources/lwm2m/credentials/clientKeyStore.jks b/application/src/test/resources/lwm2m/credentials/clientKeyStore.jks new file mode 100644 index 0000000000..7cc58589b7 Binary files /dev/null and b/application/src/test/resources/lwm2m/credentials/clientKeyStore.jks differ diff --git a/application/src/test/resources/lwm2m/credentials/serverKeyStore.jks b/application/src/test/resources/lwm2m/credentials/serverKeyStore.jks new file mode 100644 index 0000000000..f1f03005e1 Binary files /dev/null and b/application/src/test/resources/lwm2m/credentials/serverKeyStore.jks differ diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsCertificateVerifier.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsCertificateVerifier.java index 1de7bb1693..2076c7a354 100644 --- a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsCertificateVerifier.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsCertificateVerifier.java @@ -145,7 +145,6 @@ public class TbCoapDtlsCertificateVerifier implements NewAdvancedCertificateVeri @Override public void setResultHandler(HandshakeResultHandler resultHandler) { - // empty implementation } public ConcurrentMap getTbCoapDtlsSessionIdsMap() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/AbstractLwM2MClientCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/AbstractLwM2MClientCredentials.java new file mode 100644 index 0000000000..66eb523209 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/AbstractLwM2MClientCredentials.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2021 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; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public abstract class AbstractLwM2MClientCredentials implements LwM2MClientCredentials { + private String endpoint; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/HasKey.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/HasKey.java new file mode 100644 index 0000000000..ec62765298 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/HasKey.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2021 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; + +import lombok.SneakyThrows; +import org.apache.commons.codec.binary.Hex; + +public abstract class HasKey extends AbstractLwM2MClientCredentials { + private byte[] key; + + @SneakyThrows + public void setKey(String key) { + if (key != null) { + this.key = Hex.decodeHex(key.toLowerCase().toCharArray()); + } + } + + public byte[] getKey() { + return key; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/LwM2MClientCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/LwM2MClientCredentials.java new file mode 100644 index 0000000000..adf0c2ae62 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/LwM2MClientCredentials.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2021 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; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + property = "securityConfigClientMode") +@JsonSubTypes({ + @JsonSubTypes.Type(value = NoSecClientCredentials.class, name = "NO_SEC"), + @JsonSubTypes.Type(value = PSKClientCredentials.class, name = "PSK"), + @JsonSubTypes.Type(value = RPKClientCredentials.class, name = "RPK"), + @JsonSubTypes.Type(value = X509ClientCredentials.class, name = "X509")}) +public interface LwM2MClientCredentials { + + @JsonIgnore + LwM2MSecurityMode getSecurityConfigClientMode(); + + String getEndpoint(); +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/LwM2MSecurityMode.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/LwM2MSecurityMode.java new file mode 100644 index 0000000000..802fcd7efe --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/LwM2MSecurityMode.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2021 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; + +public enum LwM2MSecurityMode { + PSK, RPK, X509, NO_SEC; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/NoSecClientCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/NoSecClientCredentials.java new file mode 100644 index 0000000000..7e54a9b63d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/NoSecClientCredentials.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2021 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; + +public class NoSecClientCredentials extends AbstractLwM2MClientCredentials { + + @Override + public LwM2MSecurityMode getSecurityConfigClientMode() { + return LwM2MSecurityMode.NO_SEC; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/PSKClientCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/PSKClientCredentials.java new file mode 100644 index 0000000000..2566af7da8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/PSKClientCredentials.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2021 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; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class PSKClientCredentials extends HasKey { + private String identity; + + @Override + public LwM2MSecurityMode getSecurityConfigClientMode() { + return LwM2MSecurityMode.PSK; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/RPKClientCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/RPKClientCredentials.java new file mode 100644 index 0000000000..fe329558f8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/RPKClientCredentials.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2021 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; + +public class RPKClientCredentials extends HasKey { + + @Override + public LwM2MSecurityMode getSecurityConfigClientMode() { + return LwM2MSecurityMode.RPK; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/X509ClientCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/X509ClientCredentials.java new file mode 100644 index 0000000000..712dcab5eb --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/X509ClientCredentials.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2021 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; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class X509ClientCredentials extends AbstractLwM2MClientCredentials { + private String cert; + + @Override + public LwM2MSecurityMode getSecurityConfigClientMode() { + return LwM2MSecurityMode.X509; + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServerConfiguration.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java similarity index 99% rename from common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServerConfiguration.java rename to common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java index 16bb97aac5..9348cb31a5 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServerConfiguration.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java @@ -65,7 +65,8 @@ import static org.thingsboard.server.transport.lwm2m.server.LwM2mNetworkConfig.g @Component @ConditionalOnExpression("('${service.type:null}'=='tb-transport' && '${transport.lwm2m.enabled:false}'=='true' && '${transport.lwm2m.bootstrap.enable:false}'=='true') || ('${service.type:null}'=='monolith' && '${transport.lwm2m.enabled:false}'=='true'&& '${transport.lwm2m.bootstrap.enable:false}'=='true')") @RequiredArgsConstructor -public class LwM2MTransportBootstrapServerConfiguration { +//TODO: @ybondarenko please refactor this to be similar to DefaultLwM2mTransportService +public class LwM2MTransportBootstrapService { private PublicKey publicKey; private PrivateKey privateKey; private boolean pskMode = false; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java index 937257b189..2f175a6bce 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java @@ -73,17 +73,17 @@ public class LwM2MBootstrapConfig { configBs.servers.put(0, server0); /* Security Configuration (object 0) as defined in LWM2M 1.0.x TS. Bootstrap instance = 0 */ this.bootstrapServer.setBootstrapServerIs(true); - configBs.security.put(0, setServerSecuruty(this.bootstrapServer.getHost(), this.bootstrapServer.getPort(), this.bootstrapServer.isBootstrapServerIs(), this.bootstrapServer.getSecurityMode(), this.bootstrapServer.getClientPublicKeyOrId(), this.bootstrapServer.getServerPublicKey(), this.bootstrapServer.getClientSecretKey(), this.bootstrapServer.getServerId())); + configBs.security.put(0, setServerSecurity(this.bootstrapServer.getHost(), this.bootstrapServer.getPort(), this.bootstrapServer.isBootstrapServerIs(), this.bootstrapServer.getSecurityMode(), this.bootstrapServer.getClientPublicKeyOrId(), this.bootstrapServer.getServerPublicKey(), this.bootstrapServer.getClientSecretKey(), this.bootstrapServer.getServerId())); /* Security Configuration (object 0) as defined in LWM2M 1.0.x TS. Server instance = 1 */ - configBs.security.put(1, setServerSecuruty(this.lwm2mServer.getHost(), this.lwm2mServer.getPort(), this.lwm2mServer.isBootstrapServerIs(), this.lwm2mServer.getSecurityMode(), this.lwm2mServer.getClientPublicKeyOrId(), this.lwm2mServer.getServerPublicKey(), this.lwm2mServer.getClientSecretKey(), this.lwm2mServer.getServerId())); + configBs.security.put(1, setServerSecurity(this.lwm2mServer.getHost(), this.lwm2mServer.getPort(), this.lwm2mServer.isBootstrapServerIs(), this.lwm2mServer.getSecurityMode(), this.lwm2mServer.getClientPublicKeyOrId(), this.lwm2mServer.getServerPublicKey(), this.lwm2mServer.getClientSecretKey(), this.lwm2mServer.getServerId())); return configBs; } - private BootstrapConfig.ServerSecurity setServerSecuruty(String host, Integer port, boolean bootstrapServer, String securityMode, String clientPublicKey, String serverPublicKey, String secretKey, int serverId) { + private BootstrapConfig.ServerSecurity setServerSecurity(String host, Integer port, boolean bootstrapServer, SecurityMode securityMode, String clientPublicKey, String serverPublicKey, String secretKey, int serverId) { BootstrapConfig.ServerSecurity serverSecurity = new BootstrapConfig.ServerSecurity(); serverSecurity.uri = "coaps://" + host + ":" + Integer.toString(port); serverSecurity.bootstrapServer = bootstrapServer; - serverSecurity.securityMode = SecurityMode.valueOf(securityMode); + serverSecurity.securityMode = securityMode; serverSecurity.publicKeyOrId = setPublicKeyOrId(clientPublicKey, securityMode); serverSecurity.serverPublicKey = (serverPublicKey != null && !serverPublicKey.isEmpty()) ? Hex.decodeHex(serverPublicKey.toCharArray()) : new byte[]{}; serverSecurity.secretKey = (secretKey != null && !secretKey.isEmpty()) ? Hex.decodeHex(secretKey.toCharArray()) : new byte[]{}; @@ -91,9 +91,9 @@ public class LwM2MBootstrapConfig { return serverSecurity; } - private byte[] setPublicKeyOrId(String publicKeyOrIdStr, String securityMode) { + private byte[] setPublicKeyOrId(String publicKeyOrIdStr, SecurityMode securityMode) { return (publicKeyOrIdStr == null || publicKeyOrIdStr.isEmpty()) ? new byte[]{} : - SecurityMode.valueOf(securityMode).equals(SecurityMode.PSK) ? publicKeyOrIdStr.getBytes(StandardCharsets.UTF_8) : + SecurityMode.PSK.equals(securityMode) ? publicKeyOrIdStr.getBytes(StandardCharsets.UTF_8) : Hex.decodeHex(publicKeyOrIdStr.toCharArray()); } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java index 036e4c10b7..ccc2e62117 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java @@ -31,7 +31,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.lwm2m.secure.EndpointSecurityInfo; -import org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode; import org.thingsboard.server.transport.lwm2m.secure.LwM2mCredentialsSecurityInfoValidator; import org.thingsboard.server.transport.lwm2m.server.LwM2mSessionMsgListener; import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportContext; @@ -74,7 +73,7 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore { @Override public List getAllByEndpoint(String endPoint) { EndpointSecurityInfo store = lwM2MCredentialsSecurityInfoValidator.getEndpointSecurityInfo(endPoint, LwM2mTransportUtil.LwM2mTypeServer.BOOTSTRAP); - if (store.getBootstrapJsonCredential() != null && store.getSecurityMode() < LwM2MSecurityMode.DEFAULT_MODE.code) { + if (store.getBootstrapCredentialConfig() != null && store.getSecurityMode() != null) { /* add value to store from BootstrapJson */ this.setBootstrapConfigScurityInfo(store); BootstrapConfig bsConfigNew = store.getBootstrapConfig(); @@ -98,13 +97,13 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore { @Override public SecurityInfo getByIdentity(String identity) { EndpointSecurityInfo store = lwM2MCredentialsSecurityInfoValidator.getEndpointSecurityInfo(identity, LwM2mTransportUtil.LwM2mTypeServer.BOOTSTRAP); - if (store.getBootstrapJsonCredential() != null && store.getSecurityMode() < LwM2MSecurityMode.DEFAULT_MODE.code) { + if (store.getBootstrapCredentialConfig() != null && store.getSecurityMode() != null) { /* add value to store from BootstrapJson */ this.setBootstrapConfigScurityInfo(store); BootstrapConfig bsConfig = store.getBootstrapConfig(); if (bsConfig.security != null) { try { - bootstrapConfigStore.add(store.getEndPoint(), bsConfig); + bootstrapConfigStore.add(store.getEndpoint(), bsConfig); } catch (InvalidConfigurationException e) { log.error("", e); } @@ -119,29 +118,29 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore { LwM2MBootstrapConfig lwM2MBootstrapConfig = this.getParametersBootstrap(store); if (lwM2MBootstrapConfig != null) { /* Security info */ - switch (SecurityMode.valueOf(lwM2MBootstrapConfig.getBootstrapServer().getSecurityMode())) { + switch (lwM2MBootstrapConfig.getBootstrapServer().getSecurityMode()) { /* Use RPK only */ case PSK: - store.setSecurityInfo(SecurityInfo.newPreSharedKeyInfo(store.getEndPoint(), + store.setSecurityInfo(SecurityInfo.newPreSharedKeyInfo(store.getEndpoint(), lwM2MBootstrapConfig.getBootstrapServer().getClientPublicKeyOrId(), Hex.decodeHex(lwM2MBootstrapConfig.getBootstrapServer().getClientSecretKey().toCharArray()))); - store.setSecurityMode(SecurityMode.PSK.code); + store.setSecurityMode(SecurityMode.PSK); break; case RPK: try { - store.setSecurityInfo(SecurityInfo.newRawPublicKeyInfo(store.getEndPoint(), + store.setSecurityInfo(SecurityInfo.newRawPublicKeyInfo(store.getEndpoint(), SecurityUtil.publicKey.decode(Hex.decodeHex(lwM2MBootstrapConfig.getBootstrapServer().getClientPublicKeyOrId().toCharArray())))); - store.setSecurityMode(SecurityMode.RPK.code); + store.setSecurityMode(SecurityMode.RPK); break; } catch (IOException | GeneralSecurityException e) { - log.error("Unable to decode Client public key for [{}] [{}]", store.getEndPoint(), e.getMessage()); + log.error("Unable to decode Client public key for [{}] [{}]", store.getEndpoint(), e.getMessage()); } case X509: - store.setSecurityInfo(SecurityInfo.newX509CertInfo(store.getEndPoint())); - store.setSecurityMode(SecurityMode.X509.code); + store.setSecurityInfo(SecurityInfo.newX509CertInfo(store.getEndpoint())); + store.setSecurityMode(SecurityMode.X509); break; case NO_SEC: - store.setSecurityMode(SecurityMode.NO_SEC.code); + store.setSecurityMode(SecurityMode.NO_SEC); store.setSecurityInfo(null); break; default: @@ -153,10 +152,9 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore { private LwM2MBootstrapConfig getParametersBootstrap(EndpointSecurityInfo store) { try { - JsonObject bootstrapJsonCredential = store.getBootstrapJsonCredential(); - if (bootstrapJsonCredential != null) { + LwM2MBootstrapConfig lwM2MBootstrapConfig = store.getBootstrapCredentialConfig(); + if (lwM2MBootstrapConfig != null) { ObjectMapper mapper = new ObjectMapper(); - LwM2MBootstrapConfig lwM2MBootstrapConfig = mapper.readValue(bootstrapJsonCredential.toString(), LwM2MBootstrapConfig.class); JsonObject bootstrapObject = getBootstrapParametersFromThingsboard(store.getDeviceProfile()); lwM2MBootstrapConfig.servers = mapper.readValue(bootstrapObject.get(SERVERS).toString(), LwM2MBootstrapServers.class); LwM2MServerBootstrap profileServerBootstrap = mapper.readValue(bootstrapObject.get(BOOTSTRAP_SERVER).toString(), LwM2MServerBootstrap.class); @@ -167,22 +165,22 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore { if (this.getValidatedSecurityMode(lwM2MBootstrapConfig.bootstrapServer, profileServerBootstrap, lwM2MBootstrapConfig.lwm2mServer, profileLwm2mServer)) { lwM2MBootstrapConfig.bootstrapServer = new LwM2MServerBootstrap(lwM2MBootstrapConfig.bootstrapServer, profileServerBootstrap); lwM2MBootstrapConfig.lwm2mServer = new LwM2MServerBootstrap(lwM2MBootstrapConfig.lwm2mServer, profileLwm2mServer); - String logMsg = String.format("%s: getParametersBootstrap: %s Access connect client with bootstrap server.", LOG_LW2M_INFO, store.getEndPoint()); + String logMsg = String.format("%s: getParametersBootstrap: %s Access connect client with bootstrap server.", LOG_LW2M_INFO, store.getEndpoint()); helper.sendParametersOnThingsboardTelemetry(helper.getKvStringtoThingsboard(LOG_LW2M_TELEMETRY, logMsg), sessionInfo); return lwM2MBootstrapConfig; } else { - log.error(" [{}] Different values SecurityMode between of client and profile.", store.getEndPoint()); - log.error("{} getParametersBootstrap: [{}] Different values SecurityMode between of client and profile.", LOG_LW2M_ERROR, store.getEndPoint()); - String logMsg = String.format("%s: getParametersBootstrap: %s Different values SecurityMode between of client and profile.", LOG_LW2M_ERROR, store.getEndPoint()); + log.error(" [{}] Different values SecurityMode between of client and profile.", store.getEndpoint()); + log.error("{} getParametersBootstrap: [{}] Different values SecurityMode between of client and profile.", LOG_LW2M_ERROR, store.getEndpoint()); + String logMsg = String.format("%s: getParametersBootstrap: %s Different values SecurityMode between of client and profile.", LOG_LW2M_ERROR, store.getEndpoint()); helper.sendParametersOnThingsboardTelemetry(helper.getKvStringtoThingsboard(LOG_LW2M_TELEMETRY, logMsg), sessionInfo); return null; } } } catch (JsonProcessingException e) { - log.error("Unable to decode Json or Certificate for [{}] [{}]", store.getEndPoint(), e.getMessage()); + log.error("Unable to decode Json or Certificate for [{}] [{}]", store.getEndpoint(), e.getMessage()); return null; } - log.error("Unable to decode Json or Certificate for [{}]", store.getEndPoint()); + log.error("Unable to decode Json or Certificate for [{}]", store.getEndpoint()); return null; } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MServerBootstrap.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MServerBootstrap.java index 9dca6057da..27d2e8c865 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MServerBootstrap.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MServerBootstrap.java @@ -32,7 +32,7 @@ public class LwM2MServerBootstrap { String host = "0.0.0.0"; Integer port = 0; - String securityMode = SecurityMode.NO_SEC.name(); + SecurityMode securityMode = SecurityMode.NO_SEC; Integer serverId = 123; boolean bootstrapServerIs = false; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java index 593b56a499..25c7766895 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java @@ -156,7 +156,7 @@ public class LwM2MTransportServerConfig implements LwM2MSecureServerConfig { keyStoreValue = KeyStore.getInstance(keyStoreType); keyStoreValue.load(inKeyStore, keyStorePassword == null ? null : keyStorePassword.toCharArray()); } catch (Exception e) { - log.trace("Unable to lookup LwM2M keystore. Reason: {}, {}" , uri, e.getMessage()); + log.info("Unable to lookup LwM2M keystore. Reason: {}, {}" , uri, e.getMessage()); } } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/EndpointSecurityInfo.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/EndpointSecurityInfo.java index 851934a806..e8d3ae3c2b 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/EndpointSecurityInfo.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/EndpointSecurityInfo.java @@ -15,24 +15,23 @@ */ package org.thingsboard.server.transport.lwm2m.secure; -import com.google.gson.JsonObject; import lombok.Data; +import org.eclipse.leshan.core.SecurityMode; import org.eclipse.leshan.server.bootstrap.BootstrapConfig; import org.eclipse.leshan.server.security.SecurityInfo; import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; - -import static org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode.DEFAULT_MODE; +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; +import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MBootstrapConfig; @Data public class EndpointSecurityInfo { - private ValidateDeviceCredentialsResponseMsg msg; + private ValidateDeviceCredentialsResponse msg; private SecurityInfo securityInfo; - private int securityMode = DEFAULT_MODE.code; + private SecurityMode securityMode; /** bootstrap */ private DeviceProfile deviceProfile; - private JsonObject bootstrapJsonCredential; - private String endPoint; + private LwM2MBootstrapConfig bootstrapCredentialConfig; + private String endpoint; private BootstrapConfig bootstrapConfig; } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LWM2MGenerationPSkRPkECC.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LWM2MGenerationPSkRPkECC.java index b99192ece3..22c5878a58 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LWM2MGenerationPSkRPkECC.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LWM2MGenerationPSkRPkECC.java @@ -33,16 +33,6 @@ import java.util.Arrays; @Slf4j public class LWM2MGenerationPSkRPkECC { - public LWM2MGenerationPSkRPkECC(Integer dtlsMode) { - switch (LwM2MSecurityMode.fromSecurityMode(dtlsMode)) { - case PSK: - generationPSkKey(); - break; - case RPK: - generationRPKECCKey(); - } - } - public LWM2MGenerationPSkRPkECC() { generationPSkKey(); generationRPKECCKey(); @@ -102,12 +92,12 @@ public class LWM2MGenerationPSkRPkECC { /* Get Curves params */ String privHex = Hex.encodeHexString(privKey.getEncoded()); log.info("\nCreating new RPK for the next start... \n" + - " Public Key (Hex): [{}]\n" + - " Private Key (Hex): [{}]" + - " public_x : [{}] \n" + - " public_y : [{}] \n" + - " private_encode : [{}] \n" + - " Elliptic Curve parameters : [{}] \n", + " Public Key (Hex): [{}]\n" + + " Private Key (Hex): [{}]" + + " public_x : [{}] \n" + + " public_y : [{}] \n" + + " private_encode : [{}] \n" + + " Elliptic Curve parameters : [{}] \n", Hex.encodeHexString(pubKey.getEncoded()), privHex, Hex.encodeHexString(x), diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2MSecurityMode.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2MSecurityMode.java deleted file mode 100644 index faf776b76c..0000000000 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2MSecurityMode.java +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright © 2016-2021 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.secure; - -public enum LwM2MSecurityMode { - - PSK(0, "psk"), - RPK(1, "rpk"), - X509(2, "x509"), - NO_SEC(3, "no_sec"), - X509_EST(4, "x509_est"), - REDIS(7, "redis"), - DEFAULT_MODE(255, "default_mode"); - - public int code; - public String subEndpoint; - - LwM2MSecurityMode(int code, String subEndpoint) { - this.code = code; - this.subEndpoint = subEndpoint; - } - - public static LwM2MSecurityMode fromSecurityMode(long code) { - return fromSecurityMode((int) code); - } - - public static LwM2MSecurityMode fromSecurityMode(int code) { - for (LwM2MSecurityMode sm : LwM2MSecurityMode.values()) { - if (sm.code == code) { - return sm; - } - } - throw new IllegalArgumentException(String.format("Unsupported security code : %d", code)); - } - - - public static LwM2MSecurityMode fromSecurityMode(String subEndpoint) { - for (LwM2MSecurityMode sm : LwM2MSecurityMode.values()) { - if (sm.subEndpoint.equals(subEndpoint)) { - return sm; - } - } - throw new IllegalArgumentException(String.format("Unsupported security subEndpoint : %d", subEndpoint)); - } -} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2mCredentialsSecurityInfoValidator.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2mCredentialsSecurityInfoValidator.java index 0263e72fb2..8d90b2a86b 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2mCredentialsSecurityInfoValidator.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2mCredentialsSecurityInfoValidator.java @@ -15,33 +15,36 @@ */ package org.thingsboard.server.transport.lwm2m.secure; -import com.google.gson.JsonObject; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.eclipse.leshan.core.util.Hex; import org.eclipse.leshan.core.util.SecurityUtil; import org.eclipse.leshan.server.security.SecurityInfo; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode; import org.thingsboard.server.common.transport.TransportServiceCallback; -import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MCredentialsRequestMsg; import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredentials; +import org.thingsboard.server.transport.lwm2m.secure.credentials.LwM2MCredentials; +import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKClientCredentials; +import org.thingsboard.server.common.data.device.credentials.lwm2m.RPKClientCredentials; import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportContext; import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil; import java.io.IOException; import java.security.GeneralSecurityException; import java.security.PublicKey; -import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import static org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode.NO_SEC; -import static org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode.PSK; -import static org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode.RPK; -import static org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode.X509; +import static org.eclipse.leshan.core.SecurityMode.NO_SEC; +import static org.eclipse.leshan.core.SecurityMode.PSK; +import static org.eclipse.leshan.core.SecurityMode.RPK; +import static org.eclipse.leshan.core.SecurityMode.X509; @Slf4j @Component @@ -52,19 +55,17 @@ public class LwM2mCredentialsSecurityInfoValidator { private final LwM2mTransportContext context; private final LwM2MTransportServerConfig config; - public EndpointSecurityInfo getEndpointSecurityInfo(String endpoint, LwM2mTransportUtil.LwM2mTypeServer keyValue) { CountDownLatch latch = new CountDownLatch(1); final EndpointSecurityInfo[] resultSecurityStore = new EndpointSecurityInfo[1]; context.getTransportService().process(ValidateDeviceLwM2MCredentialsRequestMsg.newBuilder().setCredentialsId(endpoint).build(), new TransportServiceCallback<>() { @Override - public void onSuccess(ValidateDeviceCredentialsResponseMsg msg) { - String credentialsBody = msg.getCredentialsBody(); + public void onSuccess(ValidateDeviceCredentialsResponse msg) { + String credentialsBody = msg.getCredentials(); resultSecurityStore[0] = createSecurityInfo(endpoint, credentialsBody, keyValue); resultSecurityStore[0].setMsg(msg); - Optional deviceProfileOpt = LwM2mTransportUtil.decode(msg.getProfileBody().toByteArray()); - deviceProfileOpt.ifPresent(profile -> resultSecurityStore[0].setDeviceProfile(profile)); + resultSecurityStore[0].setDeviceProfile(msg.getDeviceProfile()); latch.countDown(); } @@ -92,39 +93,32 @@ public class LwM2mCredentialsSecurityInfoValidator { */ private EndpointSecurityInfo createSecurityInfo(String endpoint, String jsonStr, LwM2mTransportUtil.LwM2mTypeServer keyValue) { EndpointSecurityInfo result = new EndpointSecurityInfo(); - JsonObject objectMsg = LwM2mTransportUtil.validateJson(jsonStr); - if (objectMsg != null && !objectMsg.isJsonNull()) { - JsonObject object = (objectMsg.has(keyValue.type) && !objectMsg.get(keyValue.type).isJsonNull()) ? objectMsg.get(keyValue.type).getAsJsonObject() : null; - /** - * Only PSK - */ - String endpointPsk = (objectMsg.has("client") - && objectMsg.get("client").getAsJsonObject().has("endpoint") - && objectMsg.get("client").getAsJsonObject().get("endpoint").isJsonPrimitive()) ? objectMsg.get("client").getAsJsonObject().get("endpoint").getAsString() : null; - endpoint = (endpointPsk == null || endpointPsk.isEmpty()) ? endpoint : endpointPsk; - if (object != null && !object.isJsonNull()) { - if (keyValue.equals(LwM2mTransportUtil.LwM2mTypeServer.BOOTSTRAP)) { - result.setBootstrapJsonCredential(object); - result.setEndPoint(endpoint); - result.setSecurityMode(LwM2MSecurityMode.fromSecurityMode(object.get("bootstrapServer").getAsJsonObject().get("securityMode").getAsString().toLowerCase()).code); - } else { - LwM2MSecurityMode lwM2MSecurityMode = LwM2MSecurityMode.fromSecurityMode(object.get("securityConfigClientMode").getAsString().toLowerCase()); - switch (lwM2MSecurityMode) { - case NO_SEC: - createClientSecurityInfoNoSec(result); - break; - case PSK: - createClientSecurityInfoPSK(result, endpoint, object); - break; - case RPK: - createClientSecurityInfoRPK(result, endpoint, object); - break; - case X509: - createClientSecurityInfoX509(result, endpoint); - break; - default: - break; - } + LwM2MCredentials credentials = JacksonUtil.fromString(jsonStr, LwM2MCredentials.class); + if (credentials != null) { + if (keyValue.equals(LwM2mTransportUtil.LwM2mTypeServer.BOOTSTRAP)) { + result.setBootstrapCredentialConfig(credentials.getBootstrap()); + if (LwM2MSecurityMode.PSK.equals(credentials.getClient().getSecurityConfigClientMode())) { + PSKClientCredentials pskClientConfig = (PSKClientCredentials) credentials.getClient(); + endpoint = StringUtils.isNotEmpty(pskClientConfig.getEndpoint()) ? pskClientConfig.getEndpoint() : endpoint; + } + result.setEndpoint(endpoint); + result.setSecurityMode(credentials.getBootstrap().getBootstrapServer().getSecurityMode()); + } else { + switch (credentials.getClient().getSecurityConfigClientMode()) { + case NO_SEC: + createClientSecurityInfoNoSec(result); + break; + case PSK: + createClientSecurityInfoPSK(result, endpoint, credentials.getClient()); + break; + case RPK: + createClientSecurityInfoRPK(result, endpoint, credentials.getClient()); + break; + case X509: + createClientSecurityInfoX509(result, endpoint, credentials.getClient()); + break; + default: + break; } } } @@ -133,19 +127,18 @@ public class LwM2mCredentialsSecurityInfoValidator { private void createClientSecurityInfoNoSec(EndpointSecurityInfo result) { result.setSecurityInfo(null); - result.setSecurityMode(NO_SEC.code); + result.setSecurityMode(NO_SEC); } - private void createClientSecurityInfoPSK(EndpointSecurityInfo result, String endpoint, JsonObject object) { - /** PSK Deserialization */ - String identity = (object.has("identity") && object.get("identity").isJsonPrimitive()) ? object.get("identity").getAsString() : null; - if (identity != null && !identity.isEmpty()) { + private void createClientSecurityInfoPSK(EndpointSecurityInfo result, String endpoint, LwM2MClientCredentials clientCredentialsConfig) { + PSKClientCredentials pskConfig = (PSKClientCredentials) clientCredentialsConfig; + if (StringUtils.isNotEmpty(pskConfig.getIdentity())) { try { - byte[] key = (object.has("key") && object.get("key").isJsonPrimitive()) ? Hex.decodeHex(object.get("key").getAsString().toCharArray()) : null; - if (key != null && key.length > 0) { + if (pskConfig.getKey() != null && pskConfig.getKey().length > 0) { + endpoint = StringUtils.isNotEmpty(pskConfig.getEndpoint()) ? pskConfig.getEndpoint() : endpoint; if (endpoint != null && !endpoint.isEmpty()) { - result.setSecurityInfo(SecurityInfo.newPreSharedKeyInfo(endpoint, identity, key)); - result.setSecurityMode(PSK.code); + result.setSecurityInfo(SecurityInfo.newPreSharedKeyInfo(endpoint, pskConfig.getIdentity(), pskConfig.getKey())); + result.setSecurityMode(PSK); } } } catch (IllegalArgumentException e) { @@ -156,13 +149,13 @@ public class LwM2mCredentialsSecurityInfoValidator { } } - private void createClientSecurityInfoRPK(EndpointSecurityInfo result, String endpoint, JsonObject object) { + private void createClientSecurityInfoRPK(EndpointSecurityInfo result, String endpoint, LwM2MClientCredentials clientCredentialsConfig) { + RPKClientCredentials rpkConfig = (RPKClientCredentials) clientCredentialsConfig; try { - if (object.has("key") && object.get("key").isJsonPrimitive()) { - byte[] rpkkey = Hex.decodeHex(object.get("key").getAsString().toLowerCase().toCharArray()); - PublicKey key = SecurityUtil.publicKey.decode(rpkkey); + if (rpkConfig.getKey() != null) { + PublicKey key = SecurityUtil.publicKey.decode(rpkConfig.getKey()); result.setSecurityInfo(SecurityInfo.newRawPublicKeyInfo(endpoint, key)); - result.setSecurityMode(RPK.code); + result.setSecurityMode(RPK); } else { log.error("Missing RPK key"); } @@ -171,8 +164,8 @@ public class LwM2mCredentialsSecurityInfoValidator { } } - private void createClientSecurityInfoX509(EndpointSecurityInfo result, String endpoint) { + private void createClientSecurityInfoX509(EndpointSecurityInfo result, String endpoint, LwM2MClientCredentials clientCredentialsConfig) { result.setSecurityInfo(SecurityInfo.newX509CertInfo(endpoint)); - result.setSecurityMode(X509.code); + result.setSecurityMode(X509); } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MAuthorizer.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MAuthorizer.java new file mode 100644 index 0000000000..7269e78b5e --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MAuthorizer.java @@ -0,0 +1,67 @@ +/** + * Copyright © 2016-2021 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.secure; + +import lombok.RequiredArgsConstructor; +import org.eclipse.leshan.core.request.Identity; +import org.eclipse.leshan.core.request.UplinkRequest; +import org.eclipse.leshan.server.registration.Registration; +import org.eclipse.leshan.server.security.Authorizer; +import org.eclipse.leshan.server.security.SecurityChecker; +import org.eclipse.leshan.server.security.SecurityInfo; +import org.springframework.stereotype.Component; +import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; +import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2MDtlsSessionStore; +import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2mSecurityStore; + +@Component +@RequiredArgsConstructor +@TbLwM2mTransportComponent +public class TbLwM2MAuthorizer implements Authorizer { + + private final TbLwM2MDtlsSessionStore sessionStorage; + private final TbLwM2mSecurityStore securityStore; + private final SecurityChecker securityChecker = new SecurityChecker(); + private final LwM2mClientContext clientContext; + + @Override + public Registration isAuthorized(UplinkRequest request, Registration registration, Identity senderIdentity) { + if (senderIdentity.isX509()) { + TbX509DtlsSessionInfo sessionInfo = sessionStorage.get(registration.getEndpoint()); + if (sessionInfo != null) { + if (sessionInfo.getX509CommonName().endsWith(senderIdentity.getX509CommonName())) { + clientContext.registerClient(registration, sessionInfo.getCredentials()); + // X509 certificate is valid and matches endpoint. + return registration; + } else { + // X509 certificate is not valid. + return null; + } + } + // If session info is not found, this may be the trusted certificate, so we still need to check all other options below. + } + SecurityInfo expectedSecurityInfo = null; + if (securityStore != null) { + expectedSecurityInfo = securityStore.getByEndpoint(registration.getEndpoint()); + } + if (securityChecker.checkSecurityInfo(registration.getEndpoint(), senderIdentity, expectedSecurityInfo)) { + return registration; + } else { + return null; + } + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java new file mode 100644 index 0000000000..e83532d2cc --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java @@ -0,0 +1,193 @@ +/** + * Copyright © 2016-2021 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.secure; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.elements.util.CertPathUtil; +import org.eclipse.californium.scandium.dtls.AlertMessage; +import org.eclipse.californium.scandium.dtls.CertificateMessage; +import org.eclipse.californium.scandium.dtls.CertificateType; +import org.eclipse.californium.scandium.dtls.CertificateVerificationResult; +import org.eclipse.californium.scandium.dtls.ConnectionId; +import org.eclipse.californium.scandium.dtls.DTLSSession; +import org.eclipse.californium.scandium.dtls.HandshakeException; +import org.eclipse.californium.scandium.dtls.HandshakeResultHandler; +import org.eclipse.californium.scandium.dtls.x509.NewAdvancedCertificateVerifier; +import org.eclipse.californium.scandium.dtls.x509.StaticCertificateVerifier; +import org.eclipse.californium.scandium.util.ServerNames; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode; +import org.thingsboard.server.common.msg.EncryptionUtil; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.common.transport.TransportServiceCallback; +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; +import org.thingsboard.server.common.transport.util.SslUtil; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; +import org.thingsboard.server.transport.lwm2m.secure.credentials.LwM2MCredentials; +import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ClientCredentials; +import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2MDtlsSessionStore; + +import javax.annotation.PostConstruct; +import javax.security.auth.x500.X500Principal; +import java.security.PublicKey; +import java.security.cert.CertPath; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +@RequiredArgsConstructor +public class TbLwM2MDtlsCertificateVerifier implements NewAdvancedCertificateVerifier { + + private final TransportService transportService; + private final TbLwM2MDtlsSessionStore sessionStorage; + private final LwM2MTransportServerConfig config; + + @SuppressWarnings("deprecation") + private StaticCertificateVerifier staticCertificateVerifier; + + @Value("${transport.lwm2m.server.security.skip_validity_check_for_client_cert:false}") + private boolean skipValidityCheckForClientCert; + + @Override + public List getSupportedCertificateType() { + return Arrays.asList(CertificateType.X_509, CertificateType.RAW_PUBLIC_KEY); + } + + @PostConstruct + public void init() { + try { + /* by default trust all */ + X509Certificate[] trustedCertificates = new X509Certificate[0]; + if (config.getKeyStoreValue() != null) { + X509Certificate rootCAX509Cert = (X509Certificate) config.getKeyStoreValue().getCertificate(config.getRootCertificateAlias()); + if (rootCAX509Cert != null) { + trustedCertificates = new X509Certificate[1]; + trustedCertificates[0] = rootCAX509Cert; + } + } + staticCertificateVerifier = new StaticCertificateVerifier(trustedCertificates); + } catch (Exception e) { + log.info("Failed to initialize the "); + } + } + + @Override + public CertificateVerificationResult verifyCertificate(ConnectionId cid, ServerNames serverName, Boolean clientUsage, boolean truncateCertificatePath, CertificateMessage message, DTLSSession session) { + CertPath certChain = message.getCertificateChain(); + if (certChain == null) { + //We trust all RPK on this layer, and use TbLwM2MAuthorizer + PublicKey publicKey = message.getPublicKey(); + return new CertificateVerificationResult(cid, publicKey, null); + } else { + try { + boolean x509CredentialsFound = false; + CertPath certpath = message.getCertificateChain(); + X509Certificate[] chain = certpath.getCertificates().toArray(new X509Certificate[0]); + for (X509Certificate cert : chain) { + try { + if (!skipValidityCheckForClientCert) { + cert.checkValidity(); + } + + String strCert = SslUtil.getCertificateString(cert); + String sha3Hash = EncryptionUtil.getSha3Hash(strCert); + final ValidateDeviceCredentialsResponse[] deviceCredentialsResponse = new ValidateDeviceCredentialsResponse[1]; + CountDownLatch latch = new CountDownLatch(1); + transportService.process(TransportProtos.ValidateDeviceLwM2MCredentialsRequestMsg.newBuilder().setCredentialsId(sha3Hash).build(), + new TransportServiceCallback<>() { + @Override + public void onSuccess(ValidateDeviceCredentialsResponse msg) { + if (!StringUtils.isEmpty(msg.getCredentials())) { + deviceCredentialsResponse[0] = msg; + } + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + log.error(e.getMessage(), e); + latch.countDown(); + } + }); + if (latch.await(10, TimeUnit.SECONDS)) { + ValidateDeviceCredentialsResponse msg = deviceCredentialsResponse[0]; + if (msg != null && org.thingsboard.server.common.data.StringUtils.isNotEmpty(msg.getCredentials())) { + LwM2MCredentials credentials = JacksonUtil.fromString(msg.getCredentials(), LwM2MCredentials.class); + if(!credentials.getClient().getSecurityConfigClientMode().equals(LwM2MSecurityMode.X509)){ + continue; + } + X509ClientCredentials config = (X509ClientCredentials) credentials.getClient(); + String certBody = config.getCert(); + String endpoint = config.getEndpoint(); + if (strCert.equals(certBody)) { + x509CredentialsFound = true; + DeviceProfile deviceProfile = msg.getDeviceProfile(); + if (msg.hasDeviceInfo() && deviceProfile != null) { + sessionStorage.put(endpoint, new TbX509DtlsSessionInfo(cert.getSubjectX500Principal().getName(), msg)); + break; + } + } else { + log.trace("[{}][{}] Certificate mismatch. Expected: {}, Actual: {}", endpoint, sha3Hash, strCert, certBody); + } + } + } + } catch (InterruptedException | + CertificateEncodingException | + CertificateExpiredException | + CertificateNotYetValidException e) { + log.error(e.getMessage(), e); + } + } + if (!x509CredentialsFound) { + if (staticCertificateVerifier != null) { + staticCertificateVerifier.verifyCertificate(message, session); + } else { + AlertMessage alert = new AlertMessage(AlertMessage.AlertLevel.FATAL, AlertMessage.AlertDescription.INTERNAL_ERROR, + session.getPeer()); + throw new HandshakeException("x509 verification not enabled!", alert); + } + } + return new CertificateVerificationResult(cid, certpath, null); + } catch (HandshakeException e) { + log.trace("Certificate validation failed!", e); + return new CertificateVerificationResult(cid, e, null); + } + } + } + + @Override + public List getAcceptedIssuers() { + return CertPathUtil.toSubjects(null); + } + + @Override + public void setResultHandler(HandshakeResultHandler resultHandler) { + + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbX509DtlsSessionInfo.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbX509DtlsSessionInfo.java new file mode 100644 index 0000000000..1c038a9440 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbX509DtlsSessionInfo.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2021 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.secure; + +import lombok.Data; +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; + +@Data +public class TbX509DtlsSessionInfo { + + private final String x509CommonName; + private final ValidateDeviceCredentialsResponse credentials; + +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/credentials/LwM2MCredentials.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/credentials/LwM2MCredentials.java new file mode 100644 index 0000000000..bbc733b40b --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/credentials/LwM2MCredentials.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2021 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.secure.credentials; + +import lombok.Data; +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredentials; +import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MBootstrapConfig; + +@Data +public class LwM2MCredentials { + private LwM2MClientCredentials client; + private LwM2MBootstrapConfig bootstrap; +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2MTransportMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2MTransportMsgHandler.java index 8f5c531de3..986085a7c9 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2MTransportMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2MTransportMsgHandler.java @@ -63,6 +63,7 @@ import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientProfile; import org.thingsboard.server.transport.lwm2m.server.client.Lwm2mClientRpcRequest; import org.thingsboard.server.transport.lwm2m.server.client.ResultsAddKeyValueProto; import org.thingsboard.server.transport.lwm2m.server.client.ResultsAnalyzerParameters; +import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2MDtlsSessionStore; import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; import javax.annotation.PostConstruct; @@ -133,13 +134,15 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler public final LwM2mTransportServerHelper helper; private final LwM2MJsonAdaptor adaptor; private final LwM2mClientContext clientContext; - public final LwM2mTransportRequest lwM2mTransportRequest; + private final LwM2mTransportRequest lwM2mTransportRequest; + private final TbLwM2MDtlsSessionStore sessionStore; + public DefaultLwM2MTransportMsgHandler(TransportService transportService, LwM2MTransportServerConfig config, LwM2mTransportServerHelper helper, LwM2mClientContext clientContext, @Lazy LwM2mTransportRequest lwM2mTransportRequest, FirmwareDataCache firmwareDataCache, - LwM2mTransportContext context, LwM2MJsonAdaptor adaptor) { + LwM2mTransportContext context, LwM2MJsonAdaptor adaptor, TbLwM2MDtlsSessionStore sessionStore) { this.transportService = transportService; this.config = config; this.helper = helper; @@ -148,6 +151,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler this.firmwareDataCache = firmwareDataCache; this.context = context; this.adaptor = adaptor; + this.sessionStore = sessionStore; } @PostConstruct @@ -245,7 +249,8 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler try { this.setCancelObservationsAll(registration); this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client unRegistration", registration.getId()); - this.closeClientSession(registration); ; + this.closeClientSession(registration); + ; } catch (Throwable t) { log.error("[{}] endpoint [{}] error Unable un registration.", registration.getEndpoint(), t); this.sendLogsToThingsboard(LOG_LW2M_ERROR + String.format(": Client Unable un Registration, %s", t.getMessage()), registration.getId()); @@ -257,6 +262,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler SessionInfoProto sessionInfo = this.getSessionInfoOrCloseSession(registration); if (sessionInfo != null) { transportService.deregisterSession(sessionInfo); + sessionStore.remove(registration.getEndpoint()); this.doCloseSession(sessionInfo); clientContext.removeClientByRegistrationId(registration.getId()); log.info("Client close session: [{}] unReg [{}] name [{}] profile ", registration.getId(), registration.getEndpoint(), sessionInfo.getDeviceType()); @@ -326,7 +332,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler } else if (response.getContent() instanceof LwM2mObjectInstance) { value = lwM2MClient.instanceToString((LwM2mObjectInstance) response.getContent(), this.converter, pathIdVer); } else if (response.getContent() instanceof LwM2mResource) { - value = lwM2MClient.resourceToString ((LwM2mResource) response.getContent(), this.converter, pathIdVer); + value = lwM2MClient.resourceToString((LwM2mResource) response.getContent(), this.converter, pathIdVer); } String msg = String.format("%s: type operation %s path - %s value - %s", LOG_LW2M_INFO, READ, pathIdVer, value); @@ -711,16 +717,15 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler */ private void updateResourcesValue(Registration registration, LwM2mResource lwM2mResource, String path) { LwM2mClient lwM2MClient = clientContext.getOrRegister(registration); - if (lwM2MClient.saveResourceValue(path, lwM2mResource, this.config - .getModelProvider())) { + if (lwM2MClient.saveResourceValue(path, lwM2mResource, this.config.getModelProvider())) { /** version != null * set setClient_fw_info... = value **/ if (lwM2MClient.getFwUpdate().isInfoFwSwUpdate()) { - lwM2MClient.getFwUpdate().initReadValue(this, path); + lwM2MClient.getFwUpdate().initReadValue(this, lwM2mTransportRequest, path); } if (lwM2MClient.getSwUpdate().isInfoFwSwUpdate()) { - lwM2MClient.getSwUpdate().initReadValue(this, path); + lwM2MClient.getSwUpdate().initReadValue(this, lwM2mTransportRequest, path); } /** @@ -737,13 +742,13 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler && (convertPathFromObjectIdToIdVer(FW_RESULT_ID, registration).equals(path))) { if (DOWNLOADED.name().equals(lwM2MClient.getFwUpdate().getStateUpdate()) && lwM2MClient.getFwUpdate().conditionalFwExecuteStart()) { - lwM2MClient.getFwUpdate().executeFwSwWare(); + lwM2MClient.getFwUpdate().executeFwSwWare(this, lwM2mTransportRequest); } else if (UPDATING.name().equals(lwM2MClient.getFwUpdate().getStateUpdate()) && lwM2MClient.getFwUpdate().conditionalFwExecuteAfterSuccess()) { - lwM2MClient.getFwUpdate().finishFwSwUpdate(true); + lwM2MClient.getFwUpdate().finishFwSwUpdate(this, true); } else if (UPDATING.name().equals(lwM2MClient.getFwUpdate().getStateUpdate()) && lwM2MClient.getFwUpdate().conditionalFwExecuteAfterError()) { - lwM2MClient.getFwUpdate().finishFwSwUpdate(false); + lwM2MClient.getFwUpdate().finishFwSwUpdate(this, false); } } @@ -762,13 +767,13 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler && (convertPathFromObjectIdToIdVer(SW_RESULT_ID, registration).equals(path))) { if (DOWNLOADED.name().equals(lwM2MClient.getSwUpdate().getStateUpdate()) && lwM2MClient.getSwUpdate().conditionalSwUpdateExecute()) { - lwM2MClient.getSwUpdate().executeFwSwWare(); + lwM2MClient.getSwUpdate().executeFwSwWare(this, lwM2mTransportRequest); } else if (UPDATING.name().equals(lwM2MClient.getSwUpdate().getStateUpdate()) && lwM2MClient.getSwUpdate().conditionalSwExecuteAfterSuccess()) { - lwM2MClient.getSwUpdate().finishFwSwUpdate(true); + lwM2MClient.getSwUpdate().finishFwSwUpdate(this, true); } else if (UPDATING.name().equals(lwM2MClient.getSwUpdate().getStateUpdate()) && lwM2MClient.getSwUpdate().conditionalSwExecuteAfterError()) { - lwM2MClient.getSwUpdate().finishFwSwUpdate(false); + lwM2MClient.getSwUpdate().finishFwSwUpdate(this, false); } } Set paths = new HashSet<>(); @@ -1422,7 +1427,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler lwM2MClient.getFwUpdate().setCurrentVersion(response.getVersion()); lwM2MClient.getFwUpdate().setCurrentTitle(response.getTitle()); lwM2MClient.getFwUpdate().setCurrentId(new FirmwareId(new UUID(response.getFirmwareIdMSB(), response.getFirmwareIdLSB())).getId()); - lwM2MClient.getFwUpdate().sendReadObserveInfo(serviceImpl); + lwM2MClient.getFwUpdate().sendReadObserveInfo(lwM2mTransportRequest); } else { log.trace("Firmware [{}] [{}]", lwM2MClient.getDeviceName(), response.getResponseStatus().toString()); } @@ -1451,7 +1456,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler lwM2MClient.getSwUpdate().setCurrentVersion(response.getVersion()); lwM2MClient.getSwUpdate().setCurrentTitle(response.getTitle()); lwM2MClient.getSwUpdate().setCurrentId(new FirmwareId(new UUID(response.getFirmwareIdMSB(), response.getFirmwareIdLSB())).getId()); - lwM2MClient.getSwUpdate().sendReadObserveInfo(serviceImpl); + lwM2MClient.getSwUpdate().sendReadObserveInfo(lwM2mTransportRequest); } else { log.trace("Software [{}] [{}]", lwM2MClient.getDeviceName(), response.getResponseStatus().toString()); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java index c058712234..90e3e13033 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.transport.lwm2m.server; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.eclipse.californium.scandium.dtls.cipher.CipherSuite; import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeDecoder; import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeEncoder; import org.eclipse.leshan.core.util.Hex; @@ -25,15 +26,15 @@ import org.eclipse.leshan.server.californium.LeshanServer; import org.eclipse.leshan.server.californium.LeshanServerBuilder; import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore; import org.eclipse.leshan.server.model.LwM2mModelProvider; -import org.eclipse.leshan.server.security.DefaultAuthorizer; import org.eclipse.leshan.server.security.EditableSecurityStore; -import org.eclipse.leshan.server.security.SecurityChecker; import org.springframework.stereotype.Component; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.secure.LWM2MGenerationPSkRPkECC; +import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MAuthorizer; +import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MDtlsCertificateVerifier; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; @@ -42,7 +43,6 @@ import javax.annotation.PreDestroy; import java.math.BigInteger; import java.security.AlgorithmParameters; import java.security.KeyFactory; -import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; @@ -73,9 +73,10 @@ import static org.thingsboard.server.transport.lwm2m.server.LwM2mNetworkConfig.g @RequiredArgsConstructor public class DefaultLwM2mTransportService implements LwM2MTransportService { + public static final CipherSuite[] RPK_OR_X509_CIPHER_SUITES = {TLS_PSK_WITH_AES_128_CCM_8, TLS_PSK_WITH_AES_128_CBC_SHA256, TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256}; + public static final CipherSuite[] PSK_CIPHER_SUITES = {TLS_PSK_WITH_AES_128_CCM_8, TLS_PSK_WITH_AES_128_CBC_SHA256}; private PublicKey publicKey; private PrivateKey privateKey; - private boolean pskMode = false; private final LwM2mTransportContext context; private final LwM2MTransportServerConfig config; @@ -84,7 +85,8 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { private final CaliforniumRegistrationStore registrationStore; private final EditableSecurityStore securityStore; private final LwM2mClientContext lwM2mClientContext; - private ScheduledExecutorService registrationStoreExecutor; + private final TbLwM2MDtlsCertificateVerifier certificateVerifier; + private final TbLwM2MAuthorizer authorizer; private LeshanServer server; @@ -116,8 +118,6 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { } private LeshanServer getLhServer() { - this.registrationStoreExecutor = Executors.newScheduledThreadPool(this.config.getRegistrationStorePoolSize(), ThingsBoardThreadFactory.forName("LwM2M registrationStore")); - LeshanServerBuilder builder = new LeshanServerBuilder(); builder.setLocalAddress(config.getHost(), config.getPort()); builder.setLocalSecureAddress(config.getSecureHost(), config.getSecurePort()); @@ -125,10 +125,6 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { /* Use a magic converter to support bad type send by the UI. */ builder.setEncoder(new DefaultLwM2mNodeEncoder(LwM2mValueConverterImpl.getInstance())); - /* InMemoryRegistrationStore(ScheduledExecutorService schedExecutor, long cleanPeriodInSec) */ -//// InMemoryRegistrationStore registrationStore = new InMemoryRegistrationStore(this.registrationStoreExecutor, this.config.getCleanPeriodInSec()); -// builder.setRegistrationStore(registrationStore); - /* Create CoAP Config */ builder.setCoapConfig(getCoapConfig(config.getPort(), config.getSecurePort())); @@ -137,9 +133,6 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { config.setModelProvider(modelProvider); builder.setObjectModelProvider(modelProvider); - /* Create credentials */ - this.setServerWithCredentials(builder); - /* Set securityStore with new registrationStore */ builder.setSecurityStore(securityStore); builder.setRegistrationStore(registrationStore); @@ -151,18 +144,8 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { dtlsConfig.setServerOnly(true); dtlsConfig.setRecommendedSupportedGroupsOnly(config.isRecommendedSupportedGroups()); dtlsConfig.setRecommendedCipherSuitesOnly(config.isRecommendedCiphers()); - if (this.pskMode) { - dtlsConfig.setSupportedCipherSuites( - TLS_PSK_WITH_AES_128_CCM_8, - TLS_PSK_WITH_AES_128_CBC_SHA256); - } else { - dtlsConfig.setSupportedCipherSuites( - TLS_PSK_WITH_AES_128_CCM_8, - TLS_PSK_WITH_AES_128_CBC_SHA256, - TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8, - TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256); - } - + /* Create credentials */ + this.setServerWithCredentials(builder, dtlsConfig); /* Set DTLS Config */ builder.setDtlsConfig(dtlsConfig); @@ -171,40 +154,21 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { return builder.build(); } - private void setServerWithCredentials(LeshanServerBuilder builder) { - try { - if (config.getKeyStoreValue() != null) { - if (this.setBuilderX509(builder)) { - X509Certificate rootCAX509Cert = (X509Certificate) config.getKeyStoreValue().getCertificate(config.getRootCertificateAlias()); - if (rootCAX509Cert != null) { - X509Certificate[] trustedCertificates = new X509Certificate[1]; - trustedCertificates[0] = rootCAX509Cert; - builder.setTrustedCertificates(trustedCertificates); - } else { - /* by default trust all */ - builder.setTrustedCertificates(new X509Certificate[0]); - } - /* Set securityStore with registrationStore*/ - builder.setAuthorizer(new DefaultAuthorizer(securityStore, new SecurityChecker() { - @Override - protected boolean matchX509Identity(String endpoint, String receivedX509CommonName, - String expectedX509CommonName) { - return endpoint.startsWith(expectedX509CommonName); - } - })); - } - } else if (this.setServerRPK(builder)) { - this.infoPramsUri("RPK"); - this.infoParamsServerKey(this.publicKey, this.privateKey); - } else { - /* by default trust all */ - builder.setTrustedCertificates(new X509Certificate[0]); - log.info("Unable to load X509 files for LWM2MServer"); - this.pskMode = true; - this.infoPramsUri("PSK"); - } - } catch (KeyStoreException ex) { - log.error("[{}] Unable to load X509 files server", ex.getMessage()); + private void setServerWithCredentials(LeshanServerBuilder builder, DtlsConnectorConfig.Builder dtlsConfig) { + if (config.getKeyStoreValue() != null && this.setBuilderX509(builder)) { + dtlsConfig.setAdvancedCertificateVerifier(certificateVerifier); + builder.setAuthorizer(authorizer); + dtlsConfig.setSupportedCipherSuites(RPK_OR_X509_CIPHER_SUITES); + } else if (this.setServerRPK(builder)) { + this.infoPramsUri("RPK"); + this.infoParamsServerKey(this.publicKey, this.privateKey); + dtlsConfig.setSupportedCipherSuites(RPK_OR_X509_CIPHER_SUITES); + } else { + /* by default trust all */ + builder.setTrustedCertificates(new X509Certificate[0]); + log.info("Unable to load X509 files for LWM2MServer"); + dtlsConfig.setSupportedCipherSuites(PSK_CIPHER_SUITES); + this.infoPramsUri("PSK"); } } @@ -250,7 +214,7 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { private boolean setServerRPK(LeshanServerBuilder builder) { try { - this.generateKeyForRPK(); + this.loadOrGenerateRPKKeys(); if (this.publicKey != null && this.publicKey.getEncoded().length > 0 && this.privateKey != null && this.privateKey.getEncoded().length > 0) { builder.setPublicKey(this.publicKey); @@ -263,7 +227,7 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { return false; } - private void generateKeyForRPK() throws NoSuchAlgorithmException, InvalidParameterSpecException, InvalidKeySpecException { + private void loadOrGenerateRPKKeys() throws NoSuchAlgorithmException, InvalidParameterSpecException, InvalidKeySpecException { /* Get Elliptic Curve Parameter spec for secp256r1 */ AlgorithmParameters algoParameters = AlgorithmParameters.getInstance("EC"); algoParameters.init(new ECGenParameterSpec("secp256r1")); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java index 425f1ee020..c5290b5eb6 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java @@ -104,7 +104,7 @@ public class LwM2mTransportRequest { private final LwM2mTransportContext context; private final LwM2MTransportServerConfig config; private final LwM2mClientContext lwM2mClientContext; - private final DefaultLwM2MTransportMsgHandler serviceImpl; + private final DefaultLwM2MTransportMsgHandler handler; @PostConstruct public void init() { @@ -150,7 +150,7 @@ public class LwM2mTransportRequest { Lwm2mClientRpcRequest rpcRequestClone = (Lwm2mClientRpcRequest) rpcRequest.clone(); if (rpcRequestClone != null) { String errorMsg = String.format("Path %s params is not valid", targetIdVer); - serviceImpl.sentRpcRequest(rpcRequestClone, BAD_REQUEST.getName(), errorMsg, LOG_LW2M_ERROR); + handler.sentRpcRequest(rpcRequestClone, BAD_REQUEST.getName(), errorMsg, LOG_LW2M_ERROR); rpcRequest = null; } } @@ -159,12 +159,12 @@ public class LwM2mTransportRequest { if (rpcRequest != null) { ResourceModel resourceModel = lwM2MClient.getResourceModel(targetIdVer, this.config.getModelProvider()); String errorMsg = resourceModel == null ? String.format("Path %s not found in object version", targetIdVer) : "SendRequest - null"; - serviceImpl.sentRpcRequest(rpcRequest, NOT_FOUND.getName(), errorMsg, LOG_LW2M_ERROR); + handler.sentRpcRequest(rpcRequest, NOT_FOUND.getName(), errorMsg, LOG_LW2M_ERROR); } } } else if (rpcRequest != null) { String errorMsg = String.format("Path %s not found in object version", targetIdVer); - serviceImpl.sentRpcRequest(rpcRequest, NOT_FOUND.getName(), errorMsg, LOG_LW2M_ERROR); + handler.sentRpcRequest(rpcRequest, NOT_FOUND.getName(), errorMsg, LOG_LW2M_ERROR); } } else if (OBSERVE_READ_ALL.name().equals(typeOper.name()) || DISCOVER_All.name().equals(typeOper.name())) { Set paths; @@ -177,11 +177,11 @@ public class LwM2mTransportRequest { paths = Arrays.stream(objectLinks).map(Link::toString).collect(Collectors.toUnmodifiableSet()); String msg = String.format("%s: type operation %s paths - %s", LOG_LW2M_INFO, typeOper.name(), paths); - serviceImpl.sendLogsToThingsboard(msg, registration.getId()); + handler.sendLogsToThingsboard(msg, registration.getId()); } if (rpcRequest != null) { String valueMsg = String.format("Paths - %s", paths); - serviceImpl.sentRpcRequest(rpcRequest, CONTENT.name(), valueMsg, LOG_LW2M_VALUE); + handler.sentRpcRequest(rpcRequest, CONTENT.name(), valueMsg, LOG_LW2M_VALUE); } } else if (OBSERVE_CANCEL.name().equals(typeOper.name())) { int observeCancelCnt = context.getServer().getObservationService().cancelObservations(registration); @@ -192,10 +192,10 @@ public class LwM2mTransportRequest { } catch (Exception e) { String msg = String.format("%s: type operation %s %s", LOG_LW2M_ERROR, typeOper.name(), e.getMessage()); - serviceImpl.sendLogsToThingsboard(msg, registration.getId()); + handler.sendLogsToThingsboard(msg, registration.getId()); if (rpcRequest != null) { String errorMsg = String.format("Path %s type operation %s %s", targetIdVer, typeOper.name(), e.getMessage()); - serviceImpl.sentRpcRequest(rpcRequest, NOT_FOUND.getName(), errorMsg, LOG_LW2M_ERROR); + handler.sentRpcRequest(rpcRequest, NOT_FOUND.getName(), errorMsg, LOG_LW2M_ERROR); } } } @@ -318,31 +318,31 @@ public class LwM2mTransportRequest { context.getServer().send(registration, request, timeoutInMs, (ResponseCallback) response -> { if (!lwM2MClient.isInit()) { - lwM2MClient.initReadValue(this.serviceImpl, convertPathFromObjectIdToIdVer(request.getPath().toString(), registration)); + lwM2MClient.initReadValue(this.handler, convertPathFromObjectIdToIdVer(request.getPath().toString(), registration)); } if (CoAP.ResponseCode.isSuccess(((Response) response.getCoapResponse()).getCode())) { this.handleResponse(registration, request.getPath().toString(), response, request, rpcRequest); } else { String msg = String.format("%s: SendRequest %s: CoapCode - %s Lwm2m code - %d name - %s Resource path - %s", LOG_LW2M_ERROR, request.getClass().getName().toString(), ((Response) response.getCoapResponse()).getCode(), response.getCode().getCode(), response.getCode().getName(), request.getPath().toString()); - serviceImpl.sendLogsToThingsboard(msg, registration.getId()); + handler.sendLogsToThingsboard(msg, registration.getId()); log.error("[{}] [{}], [{}] - [{}] [{}] error SendRequest", request.getClass().getName().toString(), registration.getEndpoint(), ((Response) response.getCoapResponse()).getCode(), response.getCode(), request.getPath().toString()); if (!lwM2MClient.isInit()) { - lwM2MClient.initReadValue(this.serviceImpl, convertPathFromObjectIdToIdVer(request.getPath().toString(), registration)); + lwM2MClient.initReadValue(this.handler, convertPathFromObjectIdToIdVer(request.getPath().toString(), registration)); } /** Not Found */ if (rpcRequest != null) { - serviceImpl.sentRpcRequest(rpcRequest, response.getCode().getName(), response.getErrorMessage(), LOG_LW2M_ERROR); + handler.sentRpcRequest(rpcRequest, response.getCode().getName(), response.getErrorMessage(), LOG_LW2M_ERROR); } /** Not Found set setClient_fw_info... = empty **/ if (lwM2MClient.getFwUpdate().isInfoFwSwUpdate()) { - lwM2MClient.getFwUpdate().initReadValue(serviceImpl, request.getPath().toString()); + lwM2MClient.getFwUpdate().initReadValue(handler, this, request.getPath().toString()); } if (lwM2MClient.getSwUpdate().isInfoFwSwUpdate()) { - lwM2MClient.getSwUpdate().initReadValue(serviceImpl, request.getPath().toString()); + lwM2MClient.getSwUpdate().initReadValue(handler, this, request.getPath().toString()); } if (request.getPath().toString().equals(FW_PACKAGE_ID) || request.getPath().toString().equals(SW_PACKAGE_ID)) { this.afterWriteFwSWUpdateError(registration, request, response.getErrorMessage()); @@ -356,10 +356,10 @@ public class LwM2mTransportRequest { set setClient_fw_info... = empty **/ if (lwM2MClient.getFwUpdate().isInfoFwSwUpdate()) { - lwM2MClient.getFwUpdate().initReadValue(serviceImpl, request.getPath().toString()); + lwM2MClient.getFwUpdate().initReadValue(handler, this, request.getPath().toString()); } if (lwM2MClient.getSwUpdate().isInfoFwSwUpdate()) { - lwM2MClient.getSwUpdate().initReadValue(serviceImpl, request.getPath().toString()); + lwM2MClient.getSwUpdate().initReadValue(handler, this, request.getPath().toString()); } if (request.getPath().toString().equals(FW_PACKAGE_ID) || request.getPath().toString().equals(SW_PACKAGE_ID)) { this.afterWriteFwSWUpdateError(registration, request, e.getMessage()); @@ -368,14 +368,14 @@ public class LwM2mTransportRequest { this.afterExecuteFwSwUpdateError(registration, request, e.getMessage()); } if (!lwM2MClient.isInit()) { - lwM2MClient.initReadValue(this.serviceImpl, convertPathFromObjectIdToIdVer(request.getPath().toString(), registration)); + lwM2MClient.initReadValue(this.handler, convertPathFromObjectIdToIdVer(request.getPath().toString(), registration)); } String msg = String.format("%s: SendRequest %s: Resource path - %s msg error - %s", LOG_LW2M_ERROR, request.getClass().getName().toString(), request.getPath().toString(), e.getMessage()); - serviceImpl.sendLogsToThingsboard(msg, registration.getId()); + handler.sendLogsToThingsboard(msg, registration.getId()); log.error("[{}] [{}] - [{}] error SendRequest", request.getClass().getName().toString(), request.getPath().toString(), e.toString()); if (rpcRequest != null) { - serviceImpl.sentRpcRequest(rpcRequest, CoAP.CodeClass.ERROR_RESPONSE.name(), e.getMessage(), LOG_LW2M_ERROR); + handler.sentRpcRequest(rpcRequest, CoAP.CodeClass.ERROR_RESPONSE.name(), e.getMessage(), LOG_LW2M_ERROR); } }); } @@ -417,11 +417,11 @@ public class LwM2mTransportRequest { String patn = "/" + objectId + "/" + instanceId + "/" + resourceId; String msg = String.format(LOG_LW2M_ERROR + ": NumberFormatException: Resource path - %s type - %s value - %s msg error - %s SendRequest to Client", patn, type, value, e.toString()); - serviceImpl.sendLogsToThingsboard(msg, registration.getId()); + handler.sendLogsToThingsboard(msg, registration.getId()); log.error("Path: [{}] type: [{}] value: [{}] errorMsg: [{}]]", patn, type, value, e.toString()); if (rpcRequest != null) { String errorMsg = String.format("NumberFormatException: Resource path - %s type - %s value - %s", patn, type, value); - serviceImpl.sentRpcRequest(rpcRequest, BAD_REQUEST.getName(), errorMsg, LOG_LW2M_ERROR); + handler.sentRpcRequest(rpcRequest, BAD_REQUEST.getName(), errorMsg, LOG_LW2M_ERROR); } return null; } @@ -450,17 +450,17 @@ public class LwM2mTransportRequest { String pathIdVer = convertPathFromObjectIdToIdVer(path, registration); String msgLog = ""; if (response instanceof ReadResponse) { - serviceImpl.onUpdateValueAfterReadResponse(registration, pathIdVer, (ReadResponse) response, rpcRequest); + handler.onUpdateValueAfterReadResponse(registration, pathIdVer, (ReadResponse) response, rpcRequest); } else if (response instanceof DeleteResponse) { log.warn("[{}] Path [{}] DeleteResponse 5_Send", pathIdVer, response); } else if (response instanceof DiscoverResponse) { String discoverValue = Link.serialize(((DiscoverResponse)response).getObjectLinks()); msgLog = String.format("%s: type operation: %s path: %s value: %s", LOG_LW2M_INFO, DISCOVER.name(), request.getPath().toString(), discoverValue); - serviceImpl.sendLogsToThingsboard(msgLog, registration.getId()); + handler.sendLogsToThingsboard(msgLog, registration.getId()); log.warn("DiscoverResponse: [{}]", (DiscoverResponse) response); if (rpcRequest != null) { - serviceImpl.sentRpcRequest(rpcRequest, response.getCode().getName(), discoverValue, LOG_LW2M_VALUE); + handler.sentRpcRequest(rpcRequest, response.getCode().getName(), discoverValue, LOG_LW2M_VALUE); } } else if (response instanceof ExecuteResponse) { log.warn("[{}] Path [{}] ExecuteResponse 7_Send", pathIdVer, response); @@ -469,16 +469,16 @@ public class LwM2mTransportRequest { } else if (response instanceof WriteResponse) { log.warn("[{}] Path [{}] WriteResponse 9_Send", pathIdVer, response); this.infoWriteResponse(registration, response, request); - serviceImpl.onWriteResponseOk(registration, pathIdVer, (WriteRequest) request); + handler.onWriteResponseOk(registration, pathIdVer, (WriteRequest) request); } if (rpcRequest != null) { if (response instanceof ExecuteResponse || response instanceof WriteAttributesResponse || response instanceof DeleteResponse) { rpcRequest.setInfoMsg(null); - serviceImpl.sentRpcRequest(rpcRequest, response.getCode().getName(), null, null); + handler.sentRpcRequest(rpcRequest, response.getCode().getName(), null, null); } else if (response instanceof WriteResponse) { - serviceImpl.sentRpcRequest(rpcRequest, response.getCode().getName(), null, LOG_LW2M_INFO); + handler.sentRpcRequest(rpcRequest, response.getCode().getName(), null, LOG_LW2M_INFO); } } } @@ -511,7 +511,7 @@ public class LwM2mTransportRequest { LOG_LW2M_INFO, response.getCode().getCode(), request.getPath().toString(), value); } if (msg != null) { - serviceImpl.sendLogsToThingsboard(msg, registration.getId()); + handler.sendLogsToThingsboard(msg, registration.getId()); if (request.getPath().toString().equals(FW_PACKAGE_ID) || request.getPath().toString().equals(SW_PACKAGE_ID)) { this.afterWriteSuccessFwSwUpdate(registration, request); } @@ -530,11 +530,11 @@ public class LwM2mTransportRequest { LwM2mClient lwM2MClient = this.lwM2mClientContext.getClientByRegistrationId(registration.getId()); if (request.getPath().toString().equals(FW_PACKAGE_ID) && lwM2MClient.getFwUpdate() != null) { lwM2MClient.getFwUpdate().setStateUpdate(DOWNLOADED.name()); - lwM2MClient.getFwUpdate().sendLogs(WRITE_REPLACE.name(), LOG_LW2M_INFO, null); + lwM2MClient.getFwUpdate().sendLogs(handler, WRITE_REPLACE.name(), LOG_LW2M_INFO, null); } if (request.getPath().toString().equals(SW_PACKAGE_ID) && lwM2MClient.getSwUpdate() != null) { lwM2MClient.getSwUpdate().setStateUpdate(DOWNLOADED.name()); - lwM2MClient.getSwUpdate().sendLogs(WRITE_REPLACE.name(), LOG_LW2M_INFO, null); + lwM2MClient.getSwUpdate().sendLogs(handler, WRITE_REPLACE.name(), LOG_LW2M_INFO, null); } } @@ -545,30 +545,30 @@ public class LwM2mTransportRequest { LwM2mClient lwM2MClient = this.lwM2mClientContext.getClientByRegistrationId(registration.getId()); if (request.getPath().toString().equals(FW_PACKAGE_ID) && lwM2MClient.getFwUpdate() != null) { lwM2MClient.getFwUpdate().setStateUpdate(FAILED.name()); - lwM2MClient.getFwUpdate().sendLogs(WRITE_REPLACE.name(), LOG_LW2M_ERROR, msgError); + lwM2MClient.getFwUpdate().sendLogs(handler, WRITE_REPLACE.name(), LOG_LW2M_ERROR, msgError); } if (request.getPath().toString().equals(SW_PACKAGE_ID) && lwM2MClient.getSwUpdate() != null) { lwM2MClient.getSwUpdate().setStateUpdate(FAILED.name()); - lwM2MClient.getSwUpdate().sendLogs(WRITE_REPLACE.name(), LOG_LW2M_ERROR, msgError); + lwM2MClient.getSwUpdate().sendLogs(handler, WRITE_REPLACE.name(), LOG_LW2M_ERROR, msgError); } } private void afterExecuteFwSwUpdateError(Registration registration, DownlinkRequest request, String msgError) { LwM2mClient lwM2MClient = this.lwM2mClientContext.getClientByRegistrationId(registration.getId()); if (request.getPath().toString().equals(FW_UPDATE_ID) && lwM2MClient.getFwUpdate() != null) { - lwM2MClient.getFwUpdate().sendLogs(EXECUTE.name(), LOG_LW2M_ERROR, msgError); + lwM2MClient.getFwUpdate().sendLogs(handler, EXECUTE.name(), LOG_LW2M_ERROR, msgError); } if (request.getPath().toString().equals(SW_INSTALL_ID) && lwM2MClient.getSwUpdate() != null) { - lwM2MClient.getSwUpdate().sendLogs(EXECUTE.name(), LOG_LW2M_ERROR, msgError); + lwM2MClient.getSwUpdate().sendLogs(handler, EXECUTE.name(), LOG_LW2M_ERROR, msgError); } } private void afterObserveCancel(Registration registration, int observeCancelCnt, String observeCancelMsg, Lwm2mClientRpcRequest rpcRequest) { - serviceImpl.sendLogsToThingsboard(observeCancelMsg, registration.getId()); + handler.sendLogsToThingsboard(observeCancelMsg, registration.getId()); log.warn("[{}]", observeCancelMsg); if (rpcRequest != null) { rpcRequest.setInfoMsg(String.format("Count: %d", observeCancelCnt)); - serviceImpl.sentRpcRequest(rpcRequest, CONTENT.name(), null, LOG_LW2M_INFO); + handler.sentRpcRequest(rpcRequest, CONTENT.name(), null, LOG_LW2M_INFO); } } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java index 6b9049ccf6..fff35ea855 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java @@ -41,6 +41,7 @@ import org.eclipse.leshan.core.node.codec.CodecException; import org.eclipse.leshan.core.request.ContentFormat; import org.springframework.stereotype.Component; import org.thingsboard.server.common.transport.TransportServiceCallback; +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg; import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg; @@ -106,21 +107,21 @@ public class LwM2mTransportServerHelper { /** * @return - sessionInfo after access connect client */ - public SessionInfoProto getValidateSessionInfo(TransportProtos.ValidateDeviceCredentialsResponseMsg msg, long mostSignificantBits, long leastSignificantBits) { + public SessionInfoProto getValidateSessionInfo(ValidateDeviceCredentialsResponse msg, long mostSignificantBits, long leastSignificantBits) { return SessionInfoProto.newBuilder() .setNodeId(context.getNodeId()) .setSessionIdMSB(mostSignificantBits) .setSessionIdLSB(leastSignificantBits) - .setDeviceIdMSB(msg.getDeviceInfo().getDeviceIdMSB()) - .setDeviceIdLSB(msg.getDeviceInfo().getDeviceIdLSB()) - .setTenantIdMSB(msg.getDeviceInfo().getTenantIdMSB()) - .setTenantIdLSB(msg.getDeviceInfo().getTenantIdLSB()) - .setCustomerIdMSB(msg.getDeviceInfo().getCustomerIdMSB()) - .setCustomerIdLSB(msg.getDeviceInfo().getCustomerIdLSB()) + .setDeviceIdMSB(msg.getDeviceInfo().getDeviceId().getId().getMostSignificantBits()) + .setDeviceIdLSB(msg.getDeviceInfo().getDeviceId().getId().getLeastSignificantBits()) + .setTenantIdMSB(msg.getDeviceInfo().getTenantId().getId().getMostSignificantBits()) + .setTenantIdLSB(msg.getDeviceInfo().getTenantId().getId().getLeastSignificantBits()) + .setCustomerIdMSB(msg.getDeviceInfo().getCustomerId().getId().getMostSignificantBits()) + .setCustomerIdLSB(msg.getDeviceInfo().getCustomerId().getId().getLeastSignificantBits()) .setDeviceName(msg.getDeviceInfo().getDeviceName()) .setDeviceType(msg.getDeviceInfo().getDeviceType()) - .setDeviceProfileIdLSB(msg.getDeviceInfo().getDeviceProfileIdLSB()) - .setDeviceProfileIdMSB(msg.getDeviceInfo().getDeviceProfileIdMSB()) + .setDeviceProfileIdMSB(msg.getDeviceInfo().getDeviceProfileId().getId().getMostSignificantBits()) + .setDeviceProfileIdLSB(msg.getDeviceInfo().getDeviceProfileId().getId().getLeastSignificantBits()) .build(); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportUtil.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportUtil.java index 4386caebc7..c84b4a52b2 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportUtil.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportUtil.java @@ -105,7 +105,7 @@ public class LwM2mTransportUtil { public static final long DEFAULT_TIMEOUT = 2 * 60 * 1000L; // 2min in ms public static final String - LOG_LW2M_TELEMETRY = "logLwm2m"; + LOG_LW2M_TELEMETRY = "LwM2MLog"; public static final String LOG_LW2M_INFO = "info"; public static final String LOG_LW2M_ERROR = "error"; public static final String LOG_LW2M_WARN = "warn"; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java index 1969bbe84c..d3f1fb4343 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java @@ -31,10 +31,10 @@ import org.eclipse.leshan.server.registration.Registration; import org.eclipse.leshan.server.security.SecurityInfo; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.common.data.firmware.FirmwareType; import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; -import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; import org.thingsboard.server.transport.lwm2m.server.DefaultLwM2MTransportMsgHandler; import org.thingsboard.server.transport.lwm2m.server.LwM2mQueuedRequest; import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; @@ -89,7 +89,9 @@ public class LwM2mClient implements Cloneable { @Getter @Setter private Registration registration; - private ValidateDeviceCredentialsResponseMsg credentialsResponse; + + private ValidateDeviceCredentialsResponse credentials; + @Getter private final Map resources; @Getter @@ -106,11 +108,11 @@ public class LwM2mClient implements Cloneable { return super.clone(); } - public LwM2mClient(String nodeId, String endpoint, String identity, SecurityInfo securityInfo, ValidateDeviceCredentialsResponseMsg credentialsResponse, UUID profileId, UUID sessionId) { + public LwM2mClient(String nodeId, String endpoint, String identity, SecurityInfo securityInfo, ValidateDeviceCredentialsResponse credentials, UUID profileId, UUID sessionId) { this.endpoint = endpoint; this.identity = identity; this.securityInfo = securityInfo; - this.credentialsResponse = credentialsResponse; + this.credentials = credentials; this.delayedRequests = new ConcurrentHashMap<>(); this.pendingReadRequests = new CopyOnWriteArrayList<>(); this.resources = new ConcurrentHashMap<>(); @@ -118,10 +120,11 @@ public class LwM2mClient implements Cloneable { this.sessionId = sessionId; this.init = false; this.queuedRequests = new ConcurrentLinkedQueue<>(); + this.fwUpdate = new LwM2mFwSwUpdate(this, FirmwareType.FIRMWARE); this.swUpdate = new LwM2mFwSwUpdate(this, FirmwareType.SOFTWARE); - if (this.credentialsResponse != null && this.credentialsResponse.hasDeviceInfo()) { - this.session = createSession(nodeId, sessionId, credentialsResponse); + if (this.credentials != null && this.credentials.hasDeviceInfo()) { + this.session = createSession(nodeId, sessionId, credentials); this.deviceId = new UUID(session.getDeviceIdMSB(), session.getDeviceIdLSB()); this.profileId = new UUID(session.getDeviceProfileIdMSB(), session.getDeviceProfileIdLSB()); this.deviceName = session.getDeviceName(); @@ -154,21 +157,21 @@ public class LwM2mClient implements Cloneable { builder.setDeviceType(this.deviceProfileName); } - private SessionInfoProto createSession(String nodeId, UUID sessionId, ValidateDeviceCredentialsResponseMsg msg) { + private SessionInfoProto createSession(String nodeId, UUID sessionId, ValidateDeviceCredentialsResponse msg) { return SessionInfoProto.newBuilder() .setNodeId(nodeId) .setSessionIdMSB(sessionId.getMostSignificantBits()) .setSessionIdLSB(sessionId.getLeastSignificantBits()) - .setDeviceIdMSB(msg.getDeviceInfo().getDeviceIdMSB()) - .setDeviceIdLSB(msg.getDeviceInfo().getDeviceIdLSB()) - .setTenantIdMSB(msg.getDeviceInfo().getTenantIdMSB()) - .setTenantIdLSB(msg.getDeviceInfo().getTenantIdLSB()) - .setCustomerIdMSB(msg.getDeviceInfo().getCustomerIdMSB()) - .setCustomerIdLSB(msg.getDeviceInfo().getCustomerIdLSB()) + .setDeviceIdMSB(msg.getDeviceInfo().getDeviceId().getId().getMostSignificantBits()) + .setDeviceIdLSB(msg.getDeviceInfo().getDeviceId().getId().getLeastSignificantBits()) + .setTenantIdMSB(msg.getDeviceInfo().getTenantId().getId().getMostSignificantBits()) + .setTenantIdLSB(msg.getDeviceInfo().getTenantId().getId().getLeastSignificantBits()) + .setCustomerIdMSB(msg.getDeviceInfo().getCustomerId().getId().getMostSignificantBits()) + .setCustomerIdLSB(msg.getDeviceInfo().getCustomerId().getId().getLeastSignificantBits()) .setDeviceName(msg.getDeviceInfo().getDeviceName()) .setDeviceType(msg.getDeviceInfo().getDeviceType()) - .setDeviceProfileIdLSB(msg.getDeviceInfo().getDeviceProfileIdLSB()) - .setDeviceProfileIdMSB(msg.getDeviceInfo().getDeviceProfileIdMSB()) + .setDeviceProfileIdMSB(msg.getDeviceInfo().getDeviceProfileId().getId().getMostSignificantBits()) + .setDeviceProfileIdLSB(msg.getDeviceInfo().getDeviceProfileId().getId().getLeastSignificantBits()) .build(); } @@ -188,7 +191,7 @@ public class LwM2mClient implements Cloneable { } } - public Object getResourceValue (String pathRezIdVer, String pathRezId) { + public Object getResourceValue(String pathRezIdVer, String pathRezId) { String pathRez = pathRezIdVer == null ? convertPathFromObjectIdToIdVer(pathRezId, this.registration) : pathRezIdVer; if (this.resources.get(pathRez) != null) { return this.resources.get(pathRez).getLwM2mResource().isMultiInstances() ? diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java index 48c7b4db36..554a6e650c 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java @@ -17,6 +17,7 @@ package org.thingsboard.server.transport.lwm2m.server.client; import org.eclipse.leshan.server.registration.Registration; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos; import java.util.Collection; @@ -59,4 +60,6 @@ public interface LwM2mClientContext { Set getSupportedIdVerInClient(Registration registration); LwM2mClient getClientByDeviceId(UUID deviceId); + + void registerClient(Registration registration, ValidateDeviceCredentialsResponse credentials); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java index 7986019d22..0a49ea82b2 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java @@ -22,10 +22,10 @@ import org.eclipse.leshan.server.registration.Registration; import org.eclipse.leshan.server.security.EditableSecurityStore; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; import org.thingsboard.server.transport.lwm2m.secure.EndpointSecurityInfo; -import org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode; import org.thingsboard.server.transport.lwm2m.secure.LwM2mCredentialsSecurityInfoValidator; import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportContext; import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil; @@ -38,7 +38,7 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -import static org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode.NO_SEC; +import static org.eclipse.leshan.core.SecurityMode.NO_SEC; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertPathFromObjectIdToIdVer; @Slf4j @@ -112,7 +112,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { @Override public LwM2mClient fetchClientByEndpoint(String endpoint) { EndpointSecurityInfo securityInfo = lwM2MCredentialsSecurityInfoValidator.getEndpointSecurityInfo(endpoint, LwM2mTransportUtil.LwM2mTypeServer.CLIENT); - if (securityInfo.getSecurityMode() < LwM2MSecurityMode.DEFAULT_MODE.code) { + if (securityInfo.getSecurityMode() != null) { if (securityInfo.getDeviceProfile() != null) { UUID profileUuid = profileUpdate(securityInfo.getDeviceProfile())!= null ? securityInfo.getDeviceProfile().getUuidId() : null; @@ -125,7 +125,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { client = new LwM2mClient(context.getNodeId(), securityInfo.getSecurityInfo().getEndpoint(), securityInfo.getSecurityInfo().getIdentity(), securityInfo.getSecurityInfo(), securityInfo.getMsg(), profileUuid, UUID.randomUUID()); - } else if (securityInfo.getSecurityMode() == NO_SEC.code) { + } else if (NO_SEC.equals(securityInfo.getSecurityMode())) { client = new LwM2mClient(context.getNodeId(), endpoint, null, null, securityInfo.getMsg(), profileUuid, UUID.randomUUID()); @@ -142,6 +142,14 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { } } + @Override + public void registerClient(Registration registration, ValidateDeviceCredentialsResponse credentials) { + LwM2mClient client = new LwM2mClient(context.getNodeId(), registration.getEndpoint(), null, null, credentials, credentials.getDeviceProfile().getUuidId(), UUID.randomUUID()); + lwM2mClientsByEndpoint.put(registration.getEndpoint(), client); + lwM2mClientsByRegistrationId.put(registration.getId(), client); + profileUpdate(credentials.getDeviceProfile()); + } + @Override public Collection getLwM2mClients() { return lwM2mClientsByEndpoint.values(); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mFwSwUpdate.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mFwSwUpdate.java index e2bfdbcc2e..499d48a794 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mFwSwUpdate.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mFwSwUpdate.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.firmware.FirmwareType; import org.thingsboard.server.common.data.firmware.FirmwareUpdateStatus; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.lwm2m.server.DefaultLwM2MTransportMsgHandler; +import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportRequest; import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil; import java.util.ArrayList; @@ -97,7 +98,6 @@ public class LwM2mFwSwUpdate { @Setter private volatile boolean infoFwSwUpdate = false; private final FirmwareType type; - private DefaultLwM2MTransportMsgHandler serviceImpl; @Getter LwM2mClient lwM2MClient; @Getter @@ -113,7 +113,7 @@ public class LwM2mFwSwUpdate { } private void initPathId() { - if (FIRMWARE.equals(this.type) ) { + if (FIRMWARE.equals(this.type)) { this.pathPackageId = FW_PACKAGE_ID; this.pathStateId = FW_STATE_ID; this.pathResultId = FW_RESULT_ID; @@ -121,7 +121,7 @@ public class LwM2mFwSwUpdate { this.pathVerId = FW_VER_ID; this.pathInstallId = FW_UPDATE_ID; this.wUpdate = FW_UPDATE; - } else if (SOFTWARE.equals(this.type) ) { + } else if (SOFTWARE.equals(this.type)) { this.pathPackageId = SW_PACKAGE_ID; this.pathStateId = SW_UPDATE_STATE_ID; this.pathResultId = SW_RESULT_ID; @@ -133,8 +133,7 @@ public class LwM2mFwSwUpdate { } } - public void initReadValue(DefaultLwM2MTransportMsgHandler serviceImpl, String pathIdVer) { - if (this.serviceImpl == null) this.serviceImpl = serviceImpl; + public void initReadValue(DefaultLwM2MTransportMsgHandler handler, LwM2mTransportRequest request, String pathIdVer) { if (pathIdVer != null) { this.pendingInfoRequestsStart.remove(pathIdVer); } @@ -144,7 +143,7 @@ public class LwM2mFwSwUpdate { boolean conditionalStart = this.type.equals(FIRMWARE) ? this.conditionalFwUpdateStart() : this.conditionalSwUpdateStart(); if (conditionalStart) { - this.writeFwSwWare(); + this.writeFwSwWare(handler, request); } } } @@ -154,26 +153,26 @@ public class LwM2mFwSwUpdate { * Send FsSw to Lwm2mClient: * before operation Write: fw_state = DOWNLOADING */ - private void writeFwSwWare() { + private void writeFwSwWare(DefaultLwM2MTransportMsgHandler handler, LwM2mTransportRequest request) { this.stateUpdate = FirmwareUpdateStatus.DOWNLOADING.name(); // this.observeStateUpdate(); - this.sendLogs(WRITE_REPLACE.name(), LOG_LW2M_INFO, null); + this.sendLogs(handler, WRITE_REPLACE.name(), LOG_LW2M_INFO, null); int chunkSize = 0; int chunk = 0; - byte[] firmwareChunk = this.serviceImpl.firmwareDataCache.get(this.currentId.toString(), chunkSize, chunk); + byte[] firmwareChunk = handler.firmwareDataCache.get(this.currentId.toString(), chunkSize, chunk); String targetIdVer = convertPathFromObjectIdToIdVer(this.pathPackageId, this.lwM2MClient.getRegistration()); - this.serviceImpl.lwM2mTransportRequest.sendAllRequest(lwM2MClient.getRegistration(), targetIdVer, WRITE_REPLACE, ContentFormat.OPAQUE.getName(), - firmwareChunk, this.serviceImpl.config.getTimeout(), null); + request.sendAllRequest(lwM2MClient.getRegistration(), targetIdVer, WRITE_REPLACE, ContentFormat.OPAQUE.getName(), + firmwareChunk, handler.config.getTimeout(), null); } - public void sendLogs(String typeOper, String typeInfo, String msgError) { - this.sendSateOnThingsboard(); + public void sendLogs(DefaultLwM2MTransportMsgHandler handler, String typeOper, String typeInfo, String msgError) { + this.sendSateOnThingsBoard(handler); String msg = String.format("%s: %s, %s, pkgVer: %s: pkgName - %s state - %s.", typeInfo, this.wUpdate, typeOper, this.currentVersion, this.currentTitle, this.stateUpdate); if (LOG_LW2M_ERROR.equals(typeInfo)) { msg = String.format("%s Error: %s", msg, msgError); } - serviceImpl.sendLogsToThingsboard(msg, lwM2MClient.getRegistration().getId()); + handler.sendLogsToThingsboard(msg, lwM2MClient.getRegistration().getId()); } @@ -182,11 +181,11 @@ public class LwM2mFwSwUpdate { * fw_state/sw_state = UPDATING * send execute */ - public void executeFwSwWare() { - this.setStateUpdate(UPDATING.name()); - this.sendLogs(EXECUTE.name(), LOG_LW2M_INFO, null); - this.serviceImpl.lwM2mTransportRequest.sendAllRequest(this.lwM2MClient.getRegistration(), this.pathInstallId, EXECUTE, ContentFormat.TLV.getName(), - null, 0, null); + public void executeFwSwWare(DefaultLwM2MTransportMsgHandler handler, LwM2mTransportRequest request) { + this.setStateUpdate(UPDATING.name()); + this.sendLogs(handler, EXECUTE.name(), LOG_LW2M_INFO, null); + request.sendAllRequest(this.lwM2MClient.getRegistration(), this.pathInstallId, EXECUTE, ContentFormat.TLV.getName(), + null, 0, null); } @@ -219,7 +218,7 @@ public class LwM2mFwSwUpdate { */ public boolean conditionalFwExecuteStart() { Long updateResult = (Long) this.lwM2MClient.getResourceValue(null, this.pathResultId); - return LwM2mTransportUtil.UpdateResultFw.INITIAL.code == updateResult; + return LwM2mTransportUtil.UpdateResultFw.INITIAL.code == updateResult; } /** @@ -228,7 +227,7 @@ public class LwM2mFwSwUpdate { */ public boolean conditionalFwExecuteAfterSuccess() { Long updateResult = (Long) this.lwM2MClient.getResourceValue(null, this.pathResultId); - return LwM2mTransportUtil.UpdateResultFw.UPDATE_SUCCESSFULLY.code == updateResult; + return LwM2mTransportUtil.UpdateResultFw.UPDATE_SUCCESSFULLY.code == updateResult; } /** @@ -237,7 +236,7 @@ public class LwM2mFwSwUpdate { */ public boolean conditionalFwExecuteAfterError() { Long updateResult = (Long) this.lwM2MClient.getResourceValue(null, this.pathResultId); - return LwM2mTransportUtil.UpdateResultFw.UPDATE_SUCCESSFULLY.code < updateResult; + return LwM2mTransportUtil.UpdateResultFw.UPDATE_SUCCESSFULLY.code < updateResult; } /** @@ -283,21 +282,20 @@ public class LwM2mFwSwUpdate { * --- send to telemetry ( key - this is name Update Result in model) ( * -- fw_state/sw_state = FAILED */ - public void finishFwSwUpdate(boolean success) { + public void finishFwSwUpdate(DefaultLwM2MTransportMsgHandler handler, boolean success) { Long updateResult = (Long) this.lwM2MClient.getResourceValue(null, this.pathResultId); String value = FIRMWARE.equals(this.type) ? LwM2mTransportUtil.UpdateResultFw.fromUpdateResultFwByCode(updateResult.intValue()).type : LwM2mTransportUtil.UpdateResultSw.fromUpdateResultSwByCode(updateResult.intValue()).type; - String key = splitCamelCaseString((String) this.lwM2MClient.getResourceName (null, this.pathResultId)); + String key = splitCamelCaseString((String) this.lwM2MClient.getResourceName(null, this.pathResultId)); if (success) { this.stateUpdate = FirmwareUpdateStatus.UPDATED.name(); - this.sendLogs(EXECUTE.name(), LOG_LW2M_INFO, null); - } - else { + this.sendLogs(handler, EXECUTE.name(), LOG_LW2M_INFO, null); + } else { this.stateUpdate = FirmwareUpdateStatus.FAILED.name(); - this.sendLogs(EXECUTE.name(), LOG_LW2M_ERROR, value); + this.sendLogs(handler, EXECUTE.name(), LOG_LW2M_ERROR, value); } - this.serviceImpl.helper.sendParametersOnThingsboardTelemetry( - this.serviceImpl.helper.getKvStringtoThingsboard(key, value), this.lwM2MClient.getSession()); + handler.helper.sendParametersOnThingsboardTelemetry( + handler.helper.getKvStringtoThingsboard(key, value), this.lwM2MClient.getSession()); } /** @@ -306,40 +304,40 @@ public class LwM2mFwSwUpdate { */ public boolean conditionalSwExecuteAfterSuccess() { Long updateResult = (Long) this.lwM2MClient.getResourceValue(null, this.pathResultId); - return LwM2mTransportUtil.UpdateResultSw.SUCCESSFULLY_INSTALLED.code == updateResult; + return LwM2mTransportUtil.UpdateResultSw.SUCCESSFULLY_INSTALLED.code == updateResult; } + /** * After operation Execute success inspection Update Result : * >= 50 - error "NOT_ENOUGH_STORAGE" */ public boolean conditionalSwExecuteAfterError() { Long updateResult = (Long) this.lwM2MClient.getResourceValue(null, this.pathResultId); - return LwM2mTransportUtil.UpdateResultSw.NOT_ENOUGH_STORAGE.code <= updateResult; + return LwM2mTransportUtil.UpdateResultSw.NOT_ENOUGH_STORAGE.code <= updateResult; } - private void observeStateUpdate() { - this.serviceImpl.lwM2mTransportRequest.sendAllRequest(lwM2MClient.getRegistration(), + private void observeStateUpdate(DefaultLwM2MTransportMsgHandler handler, LwM2mTransportRequest request) { + request.sendAllRequest(lwM2MClient.getRegistration(), convertPathFromObjectIdToIdVer(this.pathStateId, this.lwM2MClient.getRegistration()), OBSERVE, null, null, 0, null); - this.serviceImpl.lwM2mTransportRequest.sendAllRequest(lwM2MClient.getRegistration(), + request.sendAllRequest(lwM2MClient.getRegistration(), convertPathFromObjectIdToIdVer(this.pathResultId, this.lwM2MClient.getRegistration()), OBSERVE, null, null, 0, null); } - public void sendSateOnThingsboard() { + public void sendSateOnThingsBoard(DefaultLwM2MTransportMsgHandler handler) { if (StringUtils.trimToNull(this.stateUpdate) != null) { List result = new ArrayList<>(); TransportProtos.KeyValueProto.Builder kvProto = TransportProtos.KeyValueProto.newBuilder().setKey(getAttributeKey(this.type, STATE)); kvProto.setType(TransportProtos.KeyValueType.STRING_V).setStringV(stateUpdate); result.add(kvProto.build()); - this.serviceImpl.helper.sendParametersOnThingsboardTelemetry(result, - this.serviceImpl.getSessionInfoOrCloseSession(this.lwM2MClient.getRegistration())); + handler.helper.sendParametersOnThingsboardTelemetry(result, + handler.getSessionInfoOrCloseSession(this.lwM2MClient.getRegistration())); } } - public void sendReadObserveInfo(DefaultLwM2MTransportMsgHandler serviceImpl) { + public void sendReadObserveInfo(LwM2mTransportRequest request) { this.infoFwSwUpdate = true; - this.serviceImpl = this.serviceImpl == null ? serviceImpl : this.serviceImpl; this.pendingInfoRequestsStart.add(convertPathFromObjectIdToIdVer( this.pathVerId, this.lwM2MClient.getRegistration())); this.pendingInfoRequestsStart.add(convertPathFromObjectIdToIdVer( @@ -349,7 +347,7 @@ public class LwM2mFwSwUpdate { this.pendingInfoRequestsStart.add(convertPathFromObjectIdToIdVer( this.pathResultId, this.lwM2MClient.getRegistration())); this.pendingInfoRequestsStart.forEach(pathIdVer -> { - this.serviceImpl.lwM2mTransportRequest.sendAllRequest(this.lwM2MClient.getRegistration(), pathIdVer, OBSERVE, ContentFormat.TLV.getName(), + request.sendAllRequest(this.lwM2MClient.getRegistration(), pathIdVer, OBSERVE, ContentFormat.TLV.getName(), null, 0, null); }); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbL2M2MDtlsSessionInMemoryStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbL2M2MDtlsSessionInMemoryStore.java new file mode 100644 index 0000000000..f5aa7d2e5c --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbL2M2MDtlsSessionInMemoryStore.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2021 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.server.store; + +import org.thingsboard.server.transport.lwm2m.secure.TbX509DtlsSessionInfo; + +import java.util.concurrent.ConcurrentHashMap; + +public class TbL2M2MDtlsSessionInMemoryStore implements TbLwM2MDtlsSessionStore { + + private final ConcurrentHashMap store = new ConcurrentHashMap<>(); + + @Override + public void put(String endpoint, TbX509DtlsSessionInfo msg) { + store.put(endpoint, msg); + } + + @Override + public TbX509DtlsSessionInfo get(String endpoint) { + return store.get(endpoint); + } + + @Override + public void remove(String endpoint) { + store.remove(endpoint); + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2MDtlsSessionRedisStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2MDtlsSessionRedisStore.java new file mode 100644 index 0000000000..b1c4b85e2a --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2MDtlsSessionRedisStore.java @@ -0,0 +1,66 @@ +/** + * Copyright © 2016-2021 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.server.store; + +import com.fasterxml.jackson.databind.JsonNode; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.transport.lwm2m.secure.TbX509DtlsSessionInfo; + +public class TbLwM2MDtlsSessionRedisStore implements TbLwM2MDtlsSessionStore { + + private static final String SESSION_EP = "SESSION#EP#"; + RedisConnectionFactory connectionFactory; + + public TbLwM2MDtlsSessionRedisStore(RedisConnectionFactory redisConnectionFactory) { + this.connectionFactory = redisConnectionFactory; + } + + @Override + public void put(String endpoint, TbX509DtlsSessionInfo msg) { + try (var c = connectionFactory.getConnection()) { + var msgJson = JacksonUtil.convertValue(msg, JsonNode.class); + if (msgJson != null) { + c.set(getKey(endpoint), msgJson.toString().getBytes()); + } else { + throw new RuntimeException("Problem with serialization of message: " + msg.toString()); + } + } + } + + @Override + public TbX509DtlsSessionInfo get(String endpoint) { + try (var c = connectionFactory.getConnection()) { + var data = c.get(getKey(endpoint)); + if (data != null) { + return JacksonUtil.fromString(new String(data), TbX509DtlsSessionInfo.class); + } else { + return null; + } + } + } + + @Override + public void remove(String endpoint) { + try (var c = connectionFactory.getConnection()) { + c.del(getKey(endpoint)); + } + } + + private byte[] getKey(String endpoint) { + return (SESSION_EP + endpoint).getBytes(); + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2MDtlsSessionStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2MDtlsSessionStore.java new file mode 100644 index 0000000000..3d5181232f --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2MDtlsSessionStore.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2021 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.server.store; + + +import org.thingsboard.server.transport.lwm2m.secure.TbX509DtlsSessionInfo; + +public interface TbLwM2MDtlsSessionStore { + + void put(String endpoint, TbX509DtlsSessionInfo msg); + + TbX509DtlsSessionInfo get(String endpoint); + + void remove(String endpoint); + +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mSecurityStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mSecurityStore.java index d71df62606..701d629154 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mSecurityStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mSecurityStore.java @@ -20,12 +20,16 @@ import org.eclipse.leshan.server.security.EditableSecurityStore; import org.eclipse.leshan.server.security.NonUniqueSecurityInfoException; import org.eclipse.leshan.server.security.SecurityInfo; import org.eclipse.leshan.server.security.SecurityStoreListener; +import org.springframework.stereotype.Component; +import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; import java.util.Collection; @Slf4j +@Component +@TbLwM2mTransportComponent public class TbLwM2mSecurityStore implements EditableSecurityStore { private final LwM2mClientContext clientContext; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java index 164a78b0a3..2c0c96212f 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java @@ -60,4 +60,10 @@ public class TbLwM2mStoreFactory { new TbLwM2mRedisSecurityStore(redisConfiguration.get().redisConnectionFactory()) : new InMemorySecurityStore()); } + @Bean + private TbLwM2MDtlsSessionStore sessionStore() { + return redisConfiguration.isPresent() && useRedis ? + new TbLwM2MDtlsSessionRedisStore(redisConfiguration.get().redisConnectionFactory()) : new TbL2M2MDtlsSessionInMemoryStore(); + } + } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java index 91e6be67c5..2209ffc305 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java @@ -80,7 +80,7 @@ public interface TransportService { TransportServiceCallback callback); void process(ValidateDeviceLwM2MCredentialsRequestMsg msg, - TransportServiceCallback callback); + TransportServiceCallback callback); void process(GetOrCreateDeviceFromGatewayRequestMsg msg, TransportServiceCallback callback); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java index a4e8c60841..4db4aa9bd8 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java @@ -226,19 +226,29 @@ public class JsonConverter { } private static KeyValueProto buildNumericKeyValueProto(JsonPrimitive value, String key) { - if (value.getAsString().contains(".")) { - return KeyValueProto.newBuilder() - .setKey(key) - .setType(KeyValueType.DOUBLE_V) - .setDoubleV(value.getAsDouble()) - .build(); + String valueAsString = value.getAsString(); + KeyValueProto.Builder builder = KeyValueProto.newBuilder().setKey(key); + if (valueAsString.contains("e") || valueAsString.contains("E")) { + //TODO: correct value conversion. We should make sure that if the value can't fit into Long or Double, we should send String + var bd = new BigDecimal(valueAsString); + if (bd.stripTrailingZeros().scale() <= 0) { + try { + return builder.setType(KeyValueType.LONG_V).setLongV(bd.longValueExact()).build(); + } catch (ArithmeticException e) { + return builder.setType(KeyValueType.DOUBLE_V).setDoubleV(bd.doubleValue()).build(); + } + } else { + return builder.setType(KeyValueType.DOUBLE_V).setDoubleV(bd.doubleValue()).build(); + } + } else if (valueAsString.contains(".")) { + return builder.setType(KeyValueType.DOUBLE_V).setDoubleV(value.getAsDouble()).build(); } else { try { long longValue = Long.parseLong(value.getAsString()); - return KeyValueProto.newBuilder().setKey(key).setType(KeyValueType.LONG_V) - .setLongV(longValue).build(); + return builder.setType(KeyValueType.LONG_V).setLongV(longValue).build(); } catch (NumberFormatException e) { - throw new JsonSyntaxException("Big integer values are not supported!"); + //TODO: correct value conversion. We should make sure that if the value can't fit into Long or Double, we should send String + return builder.setType(KeyValueType.DOUBLE_V).setDoubleV(new BigDecimal(valueAsString).doubleValue()).build(); } } } @@ -252,6 +262,7 @@ public class JsonConverter { String valueAsString = value.getAsString(); String key = valueEntry.getKey(); if (valueAsString.contains("e") || valueAsString.contains("E")) { + //TODO: correct value conversion. We should make sure that if the value can't fit into Long or Double, we should send String var bd = new BigDecimal(valueAsString); if (bd.stripTrailingZeros().scale() <= 0) { try { @@ -269,7 +280,8 @@ public class JsonConverter { long longValue = Long.parseLong(value.getAsString()); result.add(new LongDataEntry(key, longValue)); } catch (NumberFormatException e) { - throw new JsonSyntaxException("Big integer values are not supported!"); + //TODO: correct value conversion. We should make sure that if the value can't fit into Long or Double, we should send String + result.add(new DoubleDataEntry(key, new BigDecimal(valueAsString).doubleValue())); } } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index ac920c816c..5fb616aa9f 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -348,11 +348,25 @@ public class DefaultTransportService implements TransportService { } @Override - public void process(TransportProtos.ValidateDeviceLwM2MCredentialsRequestMsg msg, TransportServiceCallback callback) { - log.trace("Processing msg: {}", msg); - TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setValidateDeviceLwM2MCredentialsRequestMsg(msg).build()); - AsyncCallbackTemplate.withCallback(transportApiRequestTemplate.send(protoMsg), - response -> callback.onSuccess(response.getValue().getValidateCredResponseMsg()), callback::onError, transportCallbackExecutor); + public void process(TransportProtos.ValidateDeviceLwM2MCredentialsRequestMsg requestMsg, TransportServiceCallback callback) { + log.trace("Processing msg: {}", requestMsg); + TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setValidateDeviceLwM2MCredentialsRequestMsg(requestMsg).build()); + ListenableFuture response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp -> { + TransportProtos.ValidateDeviceCredentialsResponseMsg msg = tmp.getValue().getValidateCredResponseMsg(); + ValidateDeviceCredentialsResponse.ValidateDeviceCredentialsResponseBuilder result = ValidateDeviceCredentialsResponse.builder(); + if (msg.hasDeviceInfo()) { + result.credentials(msg.getCredentialsBody()); + TransportDeviceInfo tdi = getTransportDeviceInfo(msg.getDeviceInfo()); + result.deviceInfo(tdi); + ByteString profileBody = msg.getProfileBody(); + if (!profileBody.isEmpty()) { + DeviceProfile profile = deviceProfileCache.getOrCreate(tdi.getDeviceProfileId(), profileBody); + result.deviceProfile(profile); + } + } + return result.build(); + }, MoreExecutors.directExecutor()); + AsyncCallbackTemplate.withCallback(response, callback::onSuccess, callback::onError, transportCallbackExecutor); } @Override @@ -372,7 +386,7 @@ public class DefaultTransportService implements TransportService { TransportDeviceInfo tdi = getTransportDeviceInfo(msg.getDeviceInfo()); result.deviceInfo(tdi); ByteString profileBody = msg.getProfileBody(); - if (profileBody != null && !profileBody.isEmpty()) { + if (!profileBody.isEmpty()) { DeviceProfile profile = deviceProfileCache.getOrCreate(tdi.getDeviceProfileId(), profileBody); if (transportType != DeviceTransportType.DEFAULT && profile != null && profile.getTransportType() != DeviceTransportType.DEFAULT && profile.getTransportType() != transportType) { diff --git a/common/transport/transport-api/src/test/java/JsonConverterTest.java b/common/transport/transport-api/src/test/java/JsonConverterTest.java index cedbef50c9..dc28b268f5 100644 --- a/common/transport/transport-api/src/test/java/JsonConverterTest.java +++ b/common/transport/transport-api/src/test/java/JsonConverterTest.java @@ -21,6 +21,8 @@ import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import java.util.ArrayList; + @RunWith(MockitoJUnitRunner.class) public class JsonConverterTest { @@ -38,6 +40,12 @@ public class JsonConverterTest { Assert.assertEquals(10.1, result.get(0L).get(0).getDoubleValue().get(), 0.0); } + @Test + public void testParseAttributesBigDecimalAsLong() { + var result = new ArrayList<>(JsonConverter.convertToAttributes(JSON_PARSER.parse("{\"meterReadingDelta\": 1E1}"))); + Assert.assertEquals(10L, result.get(0).getLongValue().get().longValue()); + } + @Test public void testParseAsDouble() { var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 1.1}"), 0L); diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java index f9b3b23ff0..3cce5e1506 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.device; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; @@ -23,8 +24,12 @@ import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredentials; +import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKClientCredentials; +import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ClientCredentials; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -33,7 +38,6 @@ import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; -import org.thingsboard.common.util.JacksonUtil; import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CREDENTIALS_CACHE; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -76,7 +80,7 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen } private DeviceCredentials saveOrUpdate(TenantId tenantId, DeviceCredentials deviceCredentials) { - if(deviceCredentials.getCredentialsType() == null){ + if (deviceCredentials.getCredentialsType() == null) { throw new DataValidationException("Device credentials type should be specified"); } switch (deviceCredentials.getCredentialsType()) { @@ -131,7 +135,6 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen deviceCredentials.setCredentialsValue(JacksonUtil.toString(mqttCredentials)); } - private void formatCertData(DeviceCredentials deviceCredentials) { String cert = EncryptionUtil.trimNewLines(deviceCredentials.getCredentialsValue()); String sha3Hash = EncryptionUtil.getSha3Hash(cert); @@ -140,7 +143,49 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen } private void formatSimpleLwm2mCredentials(DeviceCredentials deviceCredentials) { + LwM2MClientCredentials clientCredentials; + ObjectNode json; + try { + json = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), ObjectNode.class); + if (json == null) { + throw new IllegalArgumentException(); + } + clientCredentials = JacksonUtil.convertValue(json.get("client"), LwM2MClientCredentials.class); + if (clientCredentials == null) { + throw new IllegalArgumentException(); + } + } catch (IllegalArgumentException e) { + throw new DataValidationException("Invalid credentials body for LwM2M credentials!"); + } + + String credentialsId = null; + switch (clientCredentials.getSecurityConfigClientMode()) { + case NO_SEC: + case RPK: + credentialsId = clientCredentials.getEndpoint(); + break; + case PSK: + credentialsId = ((PSKClientCredentials) clientCredentials).getIdentity(); + break; + case X509: + X509ClientCredentials x509Config = (X509ClientCredentials) clientCredentials; + if (x509Config.getCert() != null) { + String cert = EncryptionUtil.trimNewLines(x509Config.getCert()); + String sha3Hash = EncryptionUtil.getSha3Hash(cert); + x509Config.setCert(cert); + ((ObjectNode) json.get("client")).put("cert", cert); + deviceCredentials.setCredentialsValue(JacksonUtil.toString(json)); + credentialsId = sha3Hash; + } else { + credentialsId = x509Config.getEndpoint(); + } + break; + } + if (credentialsId == null) { + throw new DataValidationException("Invalid credentials body for LwM2M credentials!"); + } + deviceCredentials.setCredentialsId(credentialsId); } @Override diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials.component.html b/ui-ngx/src/app/modules/home/components/device/device-credentials.component.html index db28ff0453..0cf349a074 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-credentials.component.html +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials.component.html @@ -75,32 +75,7 @@ ('device.client-id-or-user-name-necessary' | translate) : ''">
- - device.lwm2m-key - - - {{ 'device.lwm2m-key-required' | translate }} - - - - device.lwm2m-value - - - {{ 'device.lwm2m-value-required' | translate }} - - - {{ 'device.lwm2m-value-format-error' | translate }} - -
- -
-
+ +
diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts b/ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts index 17ad828b48..19e9419829 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts @@ -34,19 +34,7 @@ import { DeviceCredentialsType } from '@shared/models/device.models'; import { Subject } from 'rxjs'; -import { distinctUntilChanged, takeUntil } from 'rxjs/operators'; -import { SecurityConfigLwm2mComponent } from '@home/components/device/security-config-lwm2m.component'; -import { - ClientSecurityConfig, - DEFAULT_END_POINT, - DeviceCredentialsDialogLwm2mData, - END_POINT, - getDefaultSecurityConfig, - JSON_ALL_CONFIG, - validateSecurityConfig -} from '@shared/models/lwm2m-security-config.models'; -import { TranslateService } from '@ngx-translate/core'; -import { MatDialog } from '@angular/material/dialog'; +import { takeUntil } from 'rxjs/operators'; import { isDefinedAndNotNull } from '@core/utils'; @Component({ @@ -84,9 +72,7 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, private propagateChange = (v: any) => {}; - constructor(public fb: FormBuilder, - private translate: TranslateService, - private dialog: MatDialog) { + constructor(public fb: FormBuilder) { this.deviceCredentialsFormGroup = this.fb.group({ credentialsType: [DeviceCredentialsType.ACCESS_TOKEN], credentialsId: [null], @@ -99,15 +85,14 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, }); this.deviceCredentialsFormGroup.get('credentialsBasic').disable(); this.deviceCredentialsFormGroup.valueChanges.pipe( - distinctUntilChanged(), takeUntil(this.destroy$) ).subscribe(() => { this.updateView(); }); this.deviceCredentialsFormGroup.get('credentialsType').valueChanges.pipe( takeUntil(this.destroy$) - ).subscribe((type) => { - this.credentialsTypeChanged(type); + ).subscribe(() => { + this.credentialsTypeChanged(); }); } @@ -128,8 +113,6 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, let credentialsValue = null; if (value.credentialsType === DeviceCredentialsType.MQTT_BASIC) { credentialsBasic = JSON.parse(value.credentialsValue) as DeviceCredentialMQTTBasic; - } else if (value.credentialsType === DeviceCredentialsType.LWM2M_CREDENTIALS) { - credentialsValue = JSON.parse(JSON.stringify(value.credentialsValue)) as ClientSecurityConfig; } else { credentialsValue = value.credentialsValue; } @@ -176,11 +159,10 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, }; } - credentialsTypeChanged(credentialsType: DeviceCredentialsType): void { - const credentialsValue = credentialsType === DeviceCredentialsType.LWM2M_CREDENTIALS ? this.lwm2mDefaultConfig : null; + credentialsTypeChanged(): void { this.deviceCredentialsFormGroup.patchValue({ credentialsId: null, - credentialsValue, + credentialsValue: null, credentialsBasic: {clientId: '', userName: '', password: ''} }); this.updateValidators(); @@ -198,14 +180,8 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, this.deviceCredentialsFormGroup.get('credentialsBasic').disable({emitEvent: false}); break; case DeviceCredentialsType.X509_CERTIFICATE: - this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([Validators.required]); - this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity({emitEvent: false}); - this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]); - this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity({emitEvent: false}); - this.deviceCredentialsFormGroup.get('credentialsBasic').disable({emitEvent: false}); - break; case DeviceCredentialsType.LWM2M_CREDENTIALS: - this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([Validators.required, this.lwm2mConfigJsonValidator]); + this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([Validators.required]); this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity({emitEvent: false}); this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]); this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity({emitEvent: false}); @@ -245,56 +221,4 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, onlySelf: true }); } - - openSecurityInfoLwM2mDialog($event: Event): void { - if ($event) { - $event.stopPropagation(); - $event.preventDefault(); - } - let credentialsValue = this.deviceCredentialsFormGroup.get('credentialsValue').value; - if (credentialsValue === null || credentialsValue.length === 0) { - credentialsValue = getDefaultSecurityConfig(); - } else { - try { - credentialsValue = JSON.parse(credentialsValue); - } catch (e) { - credentialsValue = getDefaultSecurityConfig(); - } - } - const credentialsId = this.deviceCredentialsFormGroup.get('credentialsId').value || DEFAULT_END_POINT; - this.dialog.open(SecurityConfigLwm2mComponent, { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: { - jsonAllConfig: credentialsValue, - endPoint: credentialsId - } - }).afterClosed().subscribe( - (res) => { - if (res) { - this.deviceCredentialsFormGroup.patchValue({ - credentialsValue: this.isDefaultLw2mResponse(res[JSON_ALL_CONFIG]) ? null : JSON.stringify(res[JSON_ALL_CONFIG]), - credentialsId: this.isDefaultLw2mResponse(res[END_POINT]) ? null : JSON.stringify(res[END_POINT]).split('\"').join('') - }); - this.deviceCredentialsFormGroup.get('credentialsValue').markAsDirty(); - } - } - ); - } - - private isDefaultLw2mResponse(response: object): boolean { - return Object.keys(response).length === 0 || JSON.stringify(response) === '[{}]'; - } - - private lwm2mConfigJsonValidator(control: FormControl) { - return validateSecurityConfig(control.value) ? null : {jsonError: {parsedJson: 'error'}}; - } - - private get lwm2mDefaultConfig(): string { - return JSON.stringify(getDefaultSecurityConfig(), null, 2); - } - - lwm2mCredentialsValueTooltip(flag: boolean): string { - return !flag ? '' : 'Example (mode=\"NoSec\"):\n\r ' + this.lwm2mDefaultConfig; - } } diff --git a/ui-ngx/src/app/modules/home/components/device/security-config-lwm2m-server.component.ts b/ui-ngx/src/app/modules/home/components/device/security-config-lwm2m-server.component.ts index b8e1b42ef4..d599927834 100644 --- a/ui-ngx/src/app/modules/home/components/device/security-config-lwm2m-server.component.ts +++ b/ui-ngx/src/app/modules/home/components/device/security-config-lwm2m-server.component.ts @@ -130,6 +130,8 @@ export class SecurityConfigLwm2mServerComponent implements OnDestroy, ControlVal case Lwm2mSecurityType.NO_SEC: this.serverFormGroup.get('clientPublicKeyOrId').clearValidators(); this.serverFormGroup.get('clientSecretKey').clearValidators(); + this.serverFormGroup.get('clientPublicKeyOrId').disable({emitEvent: false}); + this.serverFormGroup.get('clientSecretKey').disable(); break; case Lwm2mSecurityType.PSK: this.lenMinClientPublicKeyOrId = 0; @@ -172,5 +174,8 @@ export class SecurityConfigLwm2mServerComponent implements OnDestroy, ControlVal Validators.minLength(this.lengthClientSecretKey), Validators.maxLength(this.lengthClientSecretKey) ]); + + this.serverFormGroup.get('clientPublicKeyOrId').enable({emitEvent: false}); + this.serverFormGroup.get('clientSecretKey').enable(); } } diff --git a/ui-ngx/src/app/modules/home/components/device/security-config-lwm2m.component.html b/ui-ngx/src/app/modules/home/components/device/security-config-lwm2m.component.html index bd83cc817b..77b7601ed5 100644 --- a/ui-ngx/src/app/modules/home/components/device/security-config-lwm2m.component.html +++ b/ui-ngx/src/app/modules/home/components/device/security-config-lwm2m.component.html @@ -15,129 +15,109 @@ limitations under the License. --> -
- -

{{ title }}

- - -
-
- - device.lwm2m-security-config.endpoint - - - {{ 'device.lwm2m-security-config.endpoint-required' | translate }} - - - - - - device.lwm2m-security-config.mode - - - {{ credentialTypeLwM2MNamesMap.get(securityConfigLwM2MType[securityConfigClientMode]) }} - - - -
- - {{ 'device.lwm2m-security-config.identity' | translate }} - - - {{ 'device.lwm2m-security-config.identity-required' | translate }} - - -
- - {{ 'device.lwm2m-security-config.client-key' | translate }} - - {{key.value?.length || 0}}/{{lenMaxKeyClient}} - - {{ 'device.lwm2m-security-config.client-key-required' | translate }} - - - {{ 'device.lwm2m-security-config.client-key-pattern' | translate }} - - - {{ 'device.lwm2m-security-config.client-key-length' | translate: { - count: lenMaxKeyClient - } }} - - - - {{ 'device.lwm2m-security-config.client-certificate' | translate }} - -
- -
- - - - - {{ 'device.lwm2m-security-config.bootstrap-server' | translate }} - - - - - - - - - - - {{ 'device.lwm2m-security-config.lwm2m-server' | translate }} - - - - - - - - -
-
- - - - - - -
-
-
- - -
-
+ + + + + device.lwm2m-security-config.endpoint + + + {{ 'device.lwm2m-security-config.endpoint-required' | translate }} + + + + device.lwm2m-security-config.mode + + + {{ credentialTypeLwM2MNamesMap.get(securityConfigLwM2MType[securityConfigClientMode]) }} + + + + + {{ 'device.lwm2m-security-config.identity' | translate }} + + + {{ 'device.lwm2m-security-config.identity-required' | translate }} + + + + {{ 'device.lwm2m-security-config.client-key' | translate }} + + {{key.value?.length || 0}}/{{lenMaxKeyClient}} + + {{ 'device.lwm2m-security-config.client-key-required' | translate }} + + + {{ 'device.lwm2m-security-config.client-key-pattern' | translate }} + + + {{ 'device.lwm2m-security-config.client-key-length' | translate: { + count: lenMaxKeyClient + } }} + + + + device.lwm2m-security-config.client-public-key + + device.lwm2m-security-config.client-public-key-hint + + + + +
+ + + + + {{ 'device.lwm2m-security-config.bootstrap-server' | translate }} + + + + + + + + + + + {{ 'device.lwm2m-security-config.lwm2m-server' | translate }} + + + + + + + + +
+
+ + + + + + +
diff --git a/ui-ngx/src/app/modules/home/components/device/security-config-lwm2m.component.ts b/ui-ngx/src/app/modules/home/components/device/security-config-lwm2m.component.ts index 419576753c..eb920d6190 100644 --- a/ui-ngx/src/app/modules/home/components/device/security-config-lwm2m.component.ts +++ b/ui-ngx/src/app/modules/home/components/device/security-config-lwm2m.component.ts @@ -14,19 +14,20 @@ /// limitations under the License. /// - -import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; -import { DialogComponent } from '@shared/components/dialog.component'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { Router } from '@angular/router'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { TranslateService } from '@ngx-translate/core'; +import { Component, forwardRef, OnDestroy } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; import { - DeviceCredentialsDialogLwm2mData, - getClientSecurityConfig, - JSON_ALL_CONFIG, + getDefaultClientSecurityConfig, + getDefaultServerSecurityConfig, KEY_REGEXP_HEX_DEC, LEN_MAX_PSK, LEN_MAX_PUBLIC_KEY_RPK, @@ -34,48 +35,67 @@ import { Lwm2mSecurityType, Lwm2mSecurityTypeTranslationMap } from '@shared/models/lwm2m-security-config.models'; -import { MatTabChangeEvent } from '@angular/material/tabs'; -import { MatTab } from '@angular/material/tabs/tab'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; +import { isDefinedAndNotNull } from '@core/utils'; @Component({ selector: 'tb-security-config-lwm2m', templateUrl: './security-config-lwm2m.component.html', - styleUrls: ['./security-config-lwm2m.component.scss'] + styleUrls: ['./security-config-lwm2m.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SecurityConfigLwm2mComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => SecurityConfigLwm2mComponent), + multi: true + } + ] }) -export class SecurityConfigLwm2mComponent extends DialogComponent implements OnInit, OnDestroy { - - private destroy$ = new Subject(); +export class SecurityConfigLwm2mComponent implements ControlValueAccessor, Validator, OnDestroy { lwm2mConfigFormGroup: FormGroup; - title: string; securityConfigLwM2MType = Lwm2mSecurityType; securityConfigLwM2MTypes = Object.keys(Lwm2mSecurityType); credentialTypeLwM2MNamesMap = Lwm2mSecurityTypeTranslationMap; - formControlNameJsonAllConfig = JSON_ALL_CONFIG; - jsonAllConfig: Lwm2mSecurityConfigModels; lenMaxKeyClient = LEN_MAX_PSK; - tabPrevious: MatTab; - tabIndexPrevious = 0; - constructor(protected store: Store, - protected router: Router, - @Inject(MAT_DIALOG_DATA) public data: DeviceCredentialsDialogLwm2mData, - public dialogRef: MatDialogRef, - public fb: FormBuilder, - public translate: TranslateService) { - super(store, router, dialogRef); + private destroy$ = new Subject(); + private propagateChange = (v: any) => {}; + + constructor(private fb: FormBuilder) { + this.lwm2mConfigFormGroup = this.initLwm2mConfigForm(); } - ngOnInit() { - this.jsonAllConfig = JSON.parse(JSON.stringify(this.data.jsonAllConfig)); - this.lwm2mConfigFormGroup = this.initLwm2mConfigFormGroup(); - this.title = this.translate.instant('device.lwm2m-security-info') + ': ' + this.data.endPoint; - this.lwm2mConfigFormGroup.get('x509').disable(); - this.initClientSecurityConfig(this.lwm2mConfigFormGroup.get('jsonAllConfig').value); - this.registerDisableOnLoadFormControl(this.lwm2mConfigFormGroup.get('securityConfigClientMode')); + writeValue(obj: string) { + if (isDefinedAndNotNull(obj)) { + this.initClientSecurityConfig(JSON.parse(obj)); + } + } + + registerOnChange(fn: any) { + this.propagateChange = fn; + } + + registerOnTouched(fn: any) {} + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.lwm2mConfigFormGroup.disable({emitEvent: false}); + } else { + this.lwm2mConfigFormGroup.enable({emitEvent: false}); + } + } + + validate(): ValidationErrors | null { + return this.lwm2mConfigFormGroup.valid ? null : { + securityConfigLWm2m: false + }; } ngOnDestroy() { @@ -83,175 +103,97 @@ export class SecurityConfigLwm2mComponent extends DialogComponent { - if (jsonAllConfig.client.securityConfigClientMode !== Lwm2mSecurityType.NO_SEC) { - this.lwm2mConfigFormGroup.patchValue(jsonAllConfig.client, {emitEvent: false}); - } - this.securityConfigClientUpdateValidators(jsonAllConfig.client.securityConfigClientMode); + private initClientSecurityConfig(config: Lwm2mSecurityConfigModels): void { + this.lwm2mConfigFormGroup.patchValue(config, {emitEvent: false}); + this.securityConfigClientUpdateValidators(config.client.securityConfigClientMode); } private securityConfigClientModeChanged(type: Lwm2mSecurityType): void { - const config = getClientSecurityConfig(type, this.lwm2mConfigFormGroup.get('endPoint').value); + const config = getDefaultClientSecurityConfig(type, this.lwm2mConfigFormGroup.get('client.endpoint').value); switch (type) { case Lwm2mSecurityType.PSK: - config.identity = this.data.endPoint; - config.key = this.lwm2mConfigFormGroup.get('key').value; + config.key = this.lwm2mConfigFormGroup.get('client.key').value; break; case Lwm2mSecurityType.RPK: - config.key = this.lwm2mConfigFormGroup.get('key').value; + config.key = this.lwm2mConfigFormGroup.get('client.key').value; break; } - this.jsonAllConfig.client = config; - this.lwm2mConfigFormGroup.patchValue({ - ...config, - jsonAllConfig: this.jsonAllConfig - }, {emitEvent: false}); + this.lwm2mConfigFormGroup.get('client').patchValue(config, {emitEvent: false}); this.securityConfigClientUpdateValidators(type); } private securityConfigClientUpdateValidators = (mode: Lwm2mSecurityType): void => { switch (mode) { case Lwm2mSecurityType.NO_SEC: + this.setValidatorsNoSecX509(); + this.lwm2mConfigFormGroup.get('client.cert').disable(); + break; case Lwm2mSecurityType.X509: this.setValidatorsNoSecX509(); + this.lwm2mConfigFormGroup.get('client.cert').enable(); break; case Lwm2mSecurityType.PSK: this.lenMaxKeyClient = LEN_MAX_PSK; this.setValidatorsPskRpk(mode); + this.lwm2mConfigFormGroup.get('client.identity').enable(); break; case Lwm2mSecurityType.RPK: this.lenMaxKeyClient = LEN_MAX_PUBLIC_KEY_RPK; this.setValidatorsPskRpk(mode); + this.lwm2mConfigFormGroup.get('client.identity').disable(); break; } - this.lwm2mConfigFormGroup.get('identity').updateValueAndValidity({emitEvent: false}); - this.lwm2mConfigFormGroup.get('key').updateValueAndValidity({emitEvent: false}); + this.lwm2mConfigFormGroup.get('client.identity').updateValueAndValidity({emitEvent: false}); + this.lwm2mConfigFormGroup.get('client.key').updateValueAndValidity({emitEvent: false}); } private setValidatorsNoSecX509 = (): void => { - this.lwm2mConfigFormGroup.get('identity').setValidators([]); - this.lwm2mConfigFormGroup.get('key').setValidators([]); + this.lwm2mConfigFormGroup.get('client.identity').clearValidators(); + this.lwm2mConfigFormGroup.get('client.key').clearValidators(); + this.lwm2mConfigFormGroup.get('client.identity').disable({emitEvent: false}); + this.lwm2mConfigFormGroup.get('client.key').disable({emitEvent: false}); } private setValidatorsPskRpk = (mode: Lwm2mSecurityType): void => { if (mode === Lwm2mSecurityType.PSK) { - this.lwm2mConfigFormGroup.get('identity').setValidators([Validators.required]); + this.lwm2mConfigFormGroup.get('client.identity').setValidators([Validators.required]); } else { - this.lwm2mConfigFormGroup.get('identity').setValidators([]); - } - this.lwm2mConfigFormGroup.get('key').setValidators([Validators.required, - Validators.pattern(KEY_REGEXP_HEX_DEC), - Validators.maxLength(this.lenMaxKeyClient), Validators.minLength(this.lenMaxKeyClient)]); - } - - tabChanged = (tabChangeEvent: MatTabChangeEvent): void => { - if (this.tabIndexPrevious !== tabChangeEvent.index) { - this.upDateValueToJson(); + this.lwm2mConfigFormGroup.get('client.identity').clearValidators(); } - this.tabIndexPrevious = tabChangeEvent.index; + this.lwm2mConfigFormGroup.get('client.key').setValidators([ + Validators.required, + Validators.pattern(KEY_REGEXP_HEX_DEC), + Validators.maxLength(this.lenMaxKeyClient), + Validators.minLength(this.lenMaxKeyClient) + ]); + this.lwm2mConfigFormGroup.get('client.key').enable({emitEvent: false}); + this.lwm2mConfigFormGroup.get('client.cert').disable({emitEvent: false}); } - private upDateValueToJson(): void { - switch (this.tabIndexPrevious) { - case 0: - this.upDateValueToJsonTab0(); - break; - case 1: - this.upDateValueToJsonTab1(); - break; - } - } - - private upDateValueToJsonTab0 = (): void => { - if (this.lwm2mConfigFormGroup.get('identity').dirty && this.lwm2mConfigFormGroup.get('identity').valid || - this.lwm2mConfigFormGroup.get('key').dirty && this.lwm2mConfigFormGroup.get('key').valid) { - this.updateBootstrapSettings(); - this.upDateJsonAllConfig(); - } - } - - private upDateValueToJsonTab1 = (): void => { - const bootstrap = this.lwm2mConfigFormGroup.get('bootstrapServer').value; - if (bootstrap !== null - && this.lwm2mConfigFormGroup.get('bootstrapServer').dirty - && this.lwm2mConfigFormGroup.get('bootstrapServer').valid) { - this.jsonAllConfig.bootstrap.bootstrapServer = bootstrap; - this.upDateJsonAllConfig(); - } - const serverConfig = this.lwm2mConfigFormGroup.get('lwm2mServer').value; - if (serverConfig !== null - && this.lwm2mConfigFormGroup.get('lwm2mServer').dirty - && this.lwm2mConfigFormGroup.get('lwm2mServer').valid) { - this.jsonAllConfig.bootstrap.lwm2mServer = serverConfig; - this.upDateJsonAllConfig(); - } - } - - private updateBootstrapSettings() { - const securityMode = 'securityMode'; - this.jsonAllConfig.client.identity = this.lwm2mConfigFormGroup.get('identity').value; - this.jsonAllConfig.client.key = this.lwm2mConfigFormGroup.get('key').value; - if (this.lwm2mConfigFormGroup.get('bootstrapServer').value[securityMode] === Lwm2mSecurityType.PSK) { - this.jsonAllConfig.bootstrap.bootstrapServer.clientPublicKeyOrId = this.jsonAllConfig.client.identity; - this.jsonAllConfig.bootstrap.bootstrapServer.clientSecretKey = this.jsonAllConfig.client.key; - this.lwm2mConfigFormGroup.get('bootstrapServer').patchValue(this.jsonAllConfig.bootstrap.bootstrapServer, {emitEvent: false}); - } - if (this.lwm2mConfigFormGroup.get('lwm2mServer').value[securityMode] === Lwm2mSecurityType.PSK) { - this.jsonAllConfig.bootstrap.lwm2mServer.clientPublicKeyOrId = this.jsonAllConfig.client.identity; - this.jsonAllConfig.bootstrap.lwm2mServer.clientSecretKey = this.jsonAllConfig.client.key; - this.lwm2mConfigFormGroup.get('lwm2mServer').patchValue(this.jsonAllConfig.bootstrap.lwm2mServer, {emitEvent: false}); - } - } - - private upDateJsonAllConfig = (): void => { - this.lwm2mConfigFormGroup.patchValue({ - jsonAllConfig: this.jsonAllConfig - }, {emitEvent: false}); - } - - private initLwm2mConfigFormGroup = (): FormGroup => { - if (this.jsonAllConfig.client.securityConfigClientMode === Lwm2mSecurityType.PSK) { - this.data.endPoint = this.jsonAllConfig.client.endpoint; - } + private initLwm2mConfigForm = (): FormGroup => { const formGroup = this.fb.group({ - securityConfigClientMode: [this.jsonAllConfig.client.securityConfigClientMode], - identity: [''], - key: [''], - x509: [false], - bootstrapServer: [this.jsonAllConfig.bootstrap.bootstrapServer], - lwm2mServer: [this.jsonAllConfig.bootstrap.lwm2mServer], - endPoint: [this.data.endPoint], - jsonAllConfig: [this.jsonAllConfig] + client: this.fb.group({ + endpoint: ['', Validators.required], + securityConfigClientMode: [Lwm2mSecurityType.NO_SEC], + identity: [{value: '', disabled: true}], + key: [{value: '', disabled: true}], + cert: [{value: '', disabled: true}] + }), + bootstrap: this.fb.group({ + bootstrapServer: [getDefaultServerSecurityConfig()], + lwm2mServer: [getDefaultServerSecurityConfig()] + }) }); - formGroup.get('securityConfigClientMode').valueChanges.pipe( + formGroup.get('client.securityConfigClientMode').valueChanges.pipe( takeUntil(this.destroy$) ).subscribe((type) => { this.securityConfigClientModeChanged(type); }); - formGroup.get('endPoint').valueChanges.pipe( + formGroup.valueChanges.pipe( takeUntil(this.destroy$) - ).subscribe((endpoint) => { - if (formGroup.get('securityConfigClientMode').value === Lwm2mSecurityType.PSK) { - this.jsonAllConfig.client.endpoint = endpoint; - this.upDateJsonAllConfig(); - } + ).subscribe((value) => { + this.propagateChange(JSON.stringify(value)); }); return formGroup; } - - save(): void { - this.upDateValueToJson(); - this.data.endPoint = this.lwm2mConfigFormGroup.get('endPoint').value.split('\'').join(''); - this.data.jsonAllConfig = this.jsonAllConfig; - if (this.lwm2mConfigFormGroup.get('securityConfigClientMode').value === Lwm2mSecurityType.PSK) { - this.data.endPoint = this.data.jsonAllConfig.client.identity; - } - this.dialogRef.close(this.data); - } - - cancel(): void { - this.dialogRef.close(undefined); - } } - - diff --git a/ui-ngx/src/app/shared/models/lwm2m-security-config.models.ts b/ui-ngx/src/app/shared/models/lwm2m-security-config.models.ts index de0f2dc8a5..d3dd6d3ff9 100644 --- a/ui-ngx/src/app/shared/models/lwm2m-security-config.models.ts +++ b/ui-ngx/src/app/shared/models/lwm2m-security-config.models.ts @@ -14,21 +14,12 @@ /// limitations under the License. /// -export const JSON_ALL_CONFIG = 'jsonAllConfig'; -export const END_POINT = 'endPoint'; -export const DEFAULT_END_POINT = 'default_client_lwm2m_end_point_no_sec'; export const LEN_MAX_PSK = 64; export const LEN_MAX_PRIVATE_KEY = 134; export const LEN_MAX_PUBLIC_KEY_RPK = 182; export const LEN_MAX_PUBLIC_KEY_X509 = 3000; export const KEY_REGEXP_HEX_DEC = /^[-+]?[0-9A-Fa-f]+\.?[0-9A-Fa-f]*?$/; - -export interface DeviceCredentialsDialogLwm2mData { - jsonAllConfig?: Lwm2mSecurityConfigModels; - endPoint?: string; -} - export enum Lwm2mSecurityType { PSK = 'PSK', RPK = 'RPK', @@ -48,9 +39,9 @@ export const Lwm2mSecurityTypeTranslationMap = new Map - p.hasOwnProperty('client') && - isClientSecurityConfigType(p.client) && - p.hasOwnProperty('bootstrap') && - isBootstrapSecurityConfig(p.bootstrap); - -const isClientSecurityConfigType = (p: any): boolean => - p.hasOwnProperty('securityConfigClientMode') && - p.hasOwnProperty('endpoint') && - p.hasOwnProperty('identity') && - p.hasOwnProperty('key') && - p.hasOwnProperty('x509'); - -const isBootstrapSecurityConfig = (p: any): boolean => - p.hasOwnProperty('bootstrapServer') && - isServerSecurityConfig(p.bootstrapServer) && - p.hasOwnProperty('lwm2mServer') && - isServerSecurityConfig(p.lwm2mServer); - -const isServerSecurityConfig = (p: any): boolean => - p.hasOwnProperty('securityMode') && - p.hasOwnProperty('clientPublicKeyOrId') && - p.hasOwnProperty('clientSecretKey'); - -export function validateSecurityConfig(config: string): boolean { - try { - const securityConfig = JSON.parse(config); - return isSecurityConfigModels(securityConfig); - } catch (e) { - return false; - } -} - - diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 3da7b4f4bb..93c5c89955 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -922,15 +922,7 @@ "access-token-invalid": "Access token length must be from 1 to 20 characters.", "rsa-key": "RSA public key", "rsa-key-required": "RSA public key is required.", - "lwm2m-key": "LwM2M Security config key", - "lwm2m-key-required": "LwM2M Security config key is required.", "lwm2m-value": "LwM2M Security config", - "lwm2m-value-required": "LwM2M Security config value is required.", - "lwm2m-value-format-error": "Security config value must be in LwM2M Security config format.", - "lwm2m-endpoint": "Client endpoint/identity", - "lwm2m-security-info": "Security Config Info", - "lwm2m-value-edit": "Edit Security config", - "lwm2m-credentials-value-tip": "Edit security config json editor", "lwm2m-security-config": { "identity": "Client Identity", "identity-required": "Client Identity is required.", @@ -954,7 +946,9 @@ "client-secret-key-required": "Client Secret Key is required.", "client-secret-key-pattern": "Client Secret Key must be hexadecimal format.", "client-secret-key-length": "Client Secret Key must be {{ count }} characters.", - "config-json-tab": "Json Client Security Config" + "config-json-tab": "Json Client Security Config", + "client-public-key": "Client public key", + "client-public-key-hint": "If client public key is empty, the trusted certificate will be used" }, "client-id": "Client ID", "client-id-pattern": "Contains invalid character.",