diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index ca7673fc91..8d46cce8e1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -40,6 +40,8 @@ import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityViewInfo; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.TenantProfile; @@ -65,6 +67,7 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -85,7 +88,6 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; -import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.msg.TbMsg; @@ -108,6 +110,7 @@ import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.resource.TbResourceService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantProfileService; @@ -239,6 +242,9 @@ public abstract class BaseController { @Autowired protected PartitionService partitionService; + @Autowired + protected TbResourceService resourceService; + @Autowired protected TbQueueProducerProvider producerProvider; @@ -492,6 +498,9 @@ public abstract class BaseController { case WIDGET_TYPE: checkWidgetTypeId(new WidgetTypeId(entityId.getId()), operation); return; + case TB_RESOURCE: + checkResourceId(new TbResourceId(entityId.getId()), operation); + return; default: throw new IllegalArgumentException("Unsupported entity type: " + entityId.getEntityType()); } @@ -723,6 +732,30 @@ public abstract class BaseController { return ruleNode; } + TbResource checkResourceId(TbResourceId resourceId, Operation operation) throws ThingsboardException { + try { + validateId(resourceId, "Incorrect resourceId " + resourceId); + TbResource resource = resourceService.findResourceById(getCurrentUser().getTenantId(), resourceId); + checkNotNull(resource); + accessControlService.checkPermission(getCurrentUser(), Resource.TB_RESOURCE, operation, resourceId, resource); + return resource; + } catch (Exception e) { + throw handleException(e, false); + } + } + + TbResourceInfo checkResourceInfoId(TbResourceId resourceId, Operation operation) throws ThingsboardException { + try { + validateId(resourceId, "Incorrect resourceId " + resourceId); + TbResourceInfo resourceInfo = resourceService.findResourceInfoById(getCurrentUser().getTenantId(), resourceId); + checkNotNull(resourceInfo); + accessControlService.checkPermission(getCurrentUser(), Resource.TB_RESOURCE, operation, resourceId, resourceInfo); + return resourceInfo; + } catch (Exception e) { + throw handleException(e, false); + } + } + @SuppressWarnings("unchecked") protected I emptyId(EntityType entityType) { return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID); @@ -1042,8 +1075,8 @@ public abstract class BaseController { protected void processDashboardIdFromAdditionalInfo(ObjectNode additionalInfo, String requiredFields) throws ThingsboardException { String dashboardId = additionalInfo.has(requiredFields) ? additionalInfo.get(requiredFields).asText() : null; - if(dashboardId != null && !dashboardId.equals("null")) { - if(dashboardService.findDashboardById(getTenantId(), new DashboardId(UUID.fromString(dashboardId))) == null) { + if (dashboardId != null && !dashboardId.equals("null")) { + if (dashboardService.findDashboardById(getTenantId(), new DashboardId(UUID.fromString(dashboardId))) == null) { additionalInfo.remove(requiredFields); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 739f984a97..9c9bfa58a1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -63,6 +63,7 @@ import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.device.claim.ClaimResponse; import org.thingsboard.server.dao.device.claim.ClaimResult; +import org.thingsboard.server.dao.device.claim.ReclaimResult; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -528,6 +529,13 @@ public class DeviceController extends BaseController { if (result.getResponse().equals(ClaimResponse.SUCCESS)) { status = HttpStatus.OK; deferredResult.setResult(new ResponseEntity<>(result, status)); + + try { + logEntityAction(user, device.getId(), result.getDevice(), customerId, ActionType.ASSIGNED_TO_CUSTOMER, null, + device.getId().toString(), customerId.toString(), customerService.findCustomerById(tenantId, customerId).getName()); + } catch (ThingsboardException e) { + throw new RuntimeException(e); + } } else { status = HttpStatus.BAD_REQUEST; deferredResult.setResult(new ResponseEntity<>(result.getResponse(), status)); @@ -563,14 +571,20 @@ public class DeviceController extends BaseController { accessControlService.checkPermission(user, Resource.DEVICE, Operation.CLAIM_DEVICES, device.getId(), device); - ListenableFuture> future = claimDevicesService.reClaimDevice(tenantId, device); - Futures.addCallback(future, new FutureCallback>() { + ListenableFuture result = claimDevicesService.reClaimDevice(tenantId, device); + Futures.addCallback(result, new FutureCallback<>() { @Override - public void onSuccess(@Nullable List result) { - if (result != null) { - deferredResult.setResult(new ResponseEntity(HttpStatus.OK)); - } else { - deferredResult.setResult(new ResponseEntity(HttpStatus.BAD_REQUEST)); + public void onSuccess(ReclaimResult reclaimResult) { + deferredResult.setResult(new ResponseEntity(HttpStatus.OK)); + + Customer unassignedCustomer = reclaimResult.getUnassignedCustomer(); + if (unassignedCustomer != null) { + try { + logEntityAction(user, device.getId(), device, device.getCustomerId(), ActionType.UNASSIGNED_FROM_CUSTOMER, null, + device.getId().toString(), unassignedCustomer.getId().toString(), unassignedCustomer.getName()); + } catch (ThingsboardException e) { + throw new RuntimeException(e); + } } } diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceLwm2mController.java b/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java similarity index 68% rename from application/src/main/java/org/thingsboard/server/controller/DeviceLwm2mController.java rename to application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java index 5f2e946f10..02605ffaf0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceLwm2mController.java +++ b/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java @@ -22,7 +22,6 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.rule.engine.api.msg.DeviceNameOrTypeUpdateMsg; @@ -30,54 +29,19 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.common.data.lwm2m.LwM2mObject; import org.thingsboard.server.common.data.lwm2m.ServerSecurityConfig; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.permission.Resource; -import java.util.List; import java.util.Map; @Slf4j @RestController @TbCoreComponent @RequestMapping("/api") -public class DeviceLwm2mController extends BaseController { - - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/lwm2m/deviceProfile", params = {"sortOrder", "sortProperty"}, method = RequestMethod.GET) - @ResponseBody - public List getLwm2mListObjects(@RequestParam String sortOrder, - @RequestParam String sortProperty, - @RequestParam(required = false) int[] objectIds, - @RequestParam(required = false) String searchText) - throws ThingsboardException { - try { - return lwM2MModelsRepository.getLwm2mObjects(objectIds, searchText, sortProperty, sortOrder); - } catch (Exception e) { - throw handleException(e); - } - } - - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/lwm2m/deviceProfile/objects", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody - public PageData getLwm2mListObjects(@RequestParam int pageSize, - @RequestParam int page, - @RequestParam(required = false) String searchText, - @RequestParam(required = false) String sortProperty, - @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - PageLink pageLink = createPageLink(pageSize, page, searchText, sortProperty, sortOrder); - return checkNotNull(lwM2MModelsRepository.findDeviceLwm2mObjects(getTenantId(), pageLink)); - } catch (Exception e) { - throw handleException(e); - } - } +public class Lwm2mController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/lwm2m/deviceProfile/bootstrap/{securityMode}/{bootstrapServerIs}", method = RequestMethod.GET) diff --git a/application/src/main/java/org/thingsboard/server/controller/ResourceController.java b/application/src/main/java/org/thingsboard/server/controller/ResourceController.java deleted file mode 100644 index 1277a32fd6..0000000000 --- a/application/src/main/java/org/thingsboard/server/controller/ResourceController.java +++ /dev/null @@ -1,90 +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.controller; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.bind.annotation.RestController; -import org.thingsboard.server.common.data.Resource; -import org.thingsboard.server.common.data.ResourceType; -import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.dao.resource.ResourceService; -import org.thingsboard.server.queue.util.TbCoreComponent; - -@Slf4j -@RestController -@TbCoreComponent -@RequestMapping("/api") -public class ResourceController extends BaseController { - - private final ResourceService resourceService; - - public ResourceController(ResourceService resourceService) { - this.resourceService = resourceService; - } - - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/resource", method = RequestMethod.POST) - @ResponseBody - public Resource saveResource(Resource resource) throws ThingsboardException { - try { - resource.setTenantId(getTenantId()); - Resource savedResource = checkNotNull(resourceService.saveResource(resource)); - tbClusterService.onResourceChange(savedResource, null); - return savedResource; - } catch (Exception e) { - throw handleException(e); - } - } - - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/resource", method = RequestMethod.GET) - @ResponseBody - public PageData getResources(@RequestParam(required = false) boolean system, - @RequestParam int pageSize, - @RequestParam int page, - @RequestParam(required = false) String sortProperty, - @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - PageLink pageLink = createPageLink(pageSize, page, null, sortProperty, sortOrder); - return checkNotNull(resourceService.findResourcesByTenantId(system ? TenantId.SYS_TENANT_ID : getTenantId(), pageLink)); - } catch (Exception e) { - throw handleException(e); - } - } - - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/resource/{resourceType}/{resourceId}", method = RequestMethod.DELETE) - @ResponseBody - public void deleteResource(@PathVariable("resourceType") ResourceType resourceType, - @PathVariable("resourceId") String resourceId) throws ThingsboardException { - try { - Resource resource = checkNotNull(resourceService.getResource(getTenantId(), resourceType, resourceId)); - resourceService.deleteResource(getTenantId(), resourceType, resourceId); - tbClusterService.onResourceDeleted(resource, null); - } catch (Exception e) { - throw handleException(e); - } - } -} diff --git a/application/src/main/java/org/thingsboard/server/controller/RpcController.java b/application/src/main/java/org/thingsboard/server/controller/RpcController.java index f98bf364df..269755bd3e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RpcController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RpcController.java @@ -97,13 +97,9 @@ public class RpcController extends BaseController { private DeferredResult handleDeviceRPCRequest(boolean oneWay, DeviceId deviceId, String requestBody) throws ThingsboardException { try { JsonNode rpcRequestBody = jsonMapper.readTree(requestBody); - String requestData; - if (rpcRequestBody.get("params").isTextual()) { - requestData = rpcRequestBody.get("params").asText(); - } else { - requestData = jsonMapper.writeValueAsString(rpcRequestBody.get("params")); - } - RpcRequest cmd = new RpcRequest(rpcRequestBody.get("method").asText(), requestData); + RpcRequest cmd = new RpcRequest(rpcRequestBody.get("method").asText(), + jsonMapper.writeValueAsString(rpcRequestBody.get("params"))); + if (rpcRequestBody.has("timeout")) { cmd.setTimeout(rpcRequestBody.get("timeout").asLong()); } diff --git a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java new file mode 100644 index 0000000000..0190c3e790 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.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.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.TbResourceId; +import org.thingsboard.server.common.data.lwm2m.LwM2mObject; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.resource.TbResourceService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; + +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.StringJoiner; + +@Slf4j +@RestController +@TbCoreComponent +@RequestMapping("/api") +public class TbResourceController extends BaseController { + + public static final String RESOURCE_ID = "resourceId"; + + private final TbResourceService resourceService; + + public TbResourceController(TbResourceService resourceService) { + this.resourceService = resourceService; + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/resource/{resourceId}/download", method = RequestMethod.GET) + @ResponseBody + public ResponseEntity downloadResource(@PathVariable(RESOURCE_ID) String strResourceId) throws ThingsboardException { + checkParameter(RESOURCE_ID, strResourceId); + try { + TbResourceId resourceId = new TbResourceId(toUUID(strResourceId)); + TbResource tbResource = checkResourceId(resourceId, Operation.READ); + + ByteArrayResource resource = new ByteArrayResource(Base64.getDecoder().decode(tbResource.getData().getBytes())); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + tbResource.getFileName()) + .header("x-filename", tbResource.getFileName()) + .contentLength(resource.contentLength()) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(resource); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/resource/info/{resourceId}", method = RequestMethod.GET) + @ResponseBody + public TbResourceInfo getResourceInfoById(@PathVariable(RESOURCE_ID) String strResourceId) throws ThingsboardException { + checkParameter(RESOURCE_ID, strResourceId); + try { + TbResourceId resourceId = new TbResourceId(toUUID(strResourceId)); + return checkResourceInfoId(resourceId, Operation.READ); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/resource/{resourceId}", method = RequestMethod.GET) + @ResponseBody + public TbResource getResourceById(@PathVariable(RESOURCE_ID) String strResourceId) throws ThingsboardException { + checkParameter(RESOURCE_ID, strResourceId); + try { + TbResourceId resourceId = new TbResourceId(toUUID(strResourceId)); + return checkResourceId(resourceId, Operation.READ); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/resource", method = RequestMethod.POST) + @ResponseBody + public List saveResources(@RequestBody List resources) throws ThingsboardException { + try { + List addResources = new ArrayList<>(); + StringJoiner noSaveResources = new StringJoiner("; "); + resources.forEach(resource -> { + try { + resource.setTenantId(getTenantId()); + checkEntity(resource.getId(), resource, Resource.TB_RESOURCE); + addResources.add(addResource(resource)); + } catch (Exception e) { + noSaveResources.add(resource.getFileName()); + log.warn("Fail save resource: [{}]", resource.getFileName(), e); + } + }); + if (noSaveResources.length() > 0) { + throw new ThingsboardException(String.format("Fail save resource: %s", noSaveResources.toString()), ThingsboardErrorCode.INVALID_ARGUMENTS); + } + return addResources; + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/resource", method = RequestMethod.GET) + @ResponseBody + public PageData getResources(@RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { + return checkNotNull(resourceService.findTenantResourcesByTenantId(getTenantId(), pageLink)); + } else { + return checkNotNull(resourceService.findAllTenantResourcesByTenantId(getTenantId(), pageLink)); + } + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/resource/lwm2m/page", method = RequestMethod.GET) + @ResponseBody + public List getLwm2mListObjectsPage(@RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + PageLink pageLink = new PageLink(pageSize, page, textSearch); + return checkNotNull(resourceService.findLwM2mObjectPage(getTenantId(), sortProperty, sortOrder, pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/resource/lwm2m", method = RequestMethod.GET) + @ResponseBody + public List getLwm2mListObjects(@RequestParam String sortOrder, + @RequestParam String sortProperty, + @RequestParam(required = false) String[] objectIds) throws ThingsboardException { + try { + return checkNotNull(resourceService.findLwM2mObject(getTenantId(), sortOrder, sortProperty, objectIds)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/resource/{resourceId}", method = RequestMethod.DELETE) + @ResponseBody + public void deleteResource(@PathVariable("resourceId") String strResourceId) throws ThingsboardException { + checkParameter("resourceId", strResourceId); + try { + TbResourceId resourceId = new TbResourceId(toUUID(strResourceId)); + TbResource tbResource = checkResourceId(resourceId, Operation.DELETE); + resourceService.deleteResource(getTenantId(), resourceId); + tbClusterService.onResourceDeleted(tbResource, null); + } catch (Exception e) { + throw handleException(e); + } + } + + private TbResource addResource(TbResource resource) throws Exception { + checkEntity(resource.getId(), resource, Resource.TB_RESOURCE); + TbResource savedResource = checkNotNull(resourceService.saveResource(resource)); + tbClusterService.onResourceChange(savedResource, null); + return savedResource; + } +} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index d90b3ede59..3bc00ee5fa 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -191,9 +191,6 @@ public class ThingsboardInstallService { databaseTsUpgradeService.upgradeDatabase("3.2.1"); } databaseEntitiesUpgradeService.upgradeDatabase("3.2.1"); - log.info("Updating system data..."); - systemDataLoaderService.updateSystemWidgets(); - break; case "3.2.2": log.info("Upgrading ThingsBoard from version 3.2.2 to 3.3.0 ..."); databaseEntitiesUpgradeService.upgradeDatabase("3.2.2"); @@ -201,6 +198,7 @@ public class ThingsboardInstallService { dataUpdateService.updateData("3.2.2"); log.info("Updating system data..."); + systemDataLoaderService.updateSystemWidgets(); break; default: throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion); diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index 967f759372..158cadc1a3 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java @@ -17,35 +17,29 @@ package org.thingsboard.server.service.install; import com.fasterxml.jackson.databind.JsonNode; import lombok.extern.slf4j.Slf4j; -import org.eclipse.leshan.core.model.DDFFileParser; -import org.eclipse.leshan.core.model.DefaultDDFFileValidator; -import org.eclipse.leshan.core.model.InvalidDDFFileException; -import org.eclipse.leshan.core.model.ObjectModel; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Dashboard; -import org.thingsboard.server.common.data.Resource; import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; -import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.dashboard.DashboardService; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; -import org.thingsboard.server.dao.resource.ResourceService; +import org.thingsboard.server.dao.resource.TbResourceService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; -import java.io.ByteArrayInputStream; -import java.io.File; import java.io.IOException; import java.nio.file.DirectoryStream; import java.nio.file.Files; @@ -103,7 +97,7 @@ public class InstallScripts { private OAuth2ConfigTemplateService oAuth2TemplateService; @Autowired - private ResourceService resourceService; + private TbResourceService resourceService; private Path getTenantRuleChainsDir() { return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR); @@ -217,7 +211,6 @@ public class InstallScripts { } public void loadSystemLwm2mResources() throws Exception { -// Path modelsDir = Paths.get("/home/nick/Igor_project/thingsboard_ce_3_2_docker/thingsboard/common/transport/lwm2m/src/main/resources/models/"); Path modelsDir = Paths.get(getDataDir(), MODELS_DIR); if (Files.isDirectory(modelsDir)) { try (DirectoryStream dirStream = Files.newDirectoryStream(modelsDir, path -> path.toString().endsWith(XML_EXT))) { @@ -225,62 +218,19 @@ public class InstallScripts { path -> { try { byte[] fileBytes = Files.readAllBytes(path); - String key = getObjectModelLwm2mValid(fileBytes, path.getFileName().toString(), new DefaultDDFFileValidator()); - if (key != null) { - Resource resource = new Resource(); - resource.setTenantId(TenantId.SYS_TENANT_ID); - resource.setResourceType(ResourceType.LWM2M_MODEL); - resource.setResourceId(key); - resource.setValue(Base64.getEncoder().encodeToString(fileBytes)); - resourceService.saveResource(resource); - } + TbResource resource = new TbResource(); + resource.setFileName(path.getFileName().toString()); + resource.setTenantId(TenantId.SYS_TENANT_ID); + resource.setResourceType(ResourceType.LWM2M_MODEL); + resource.setData(Base64.getEncoder().encodeToString(fileBytes)); + resourceService.saveResource(resource); } catch (Exception e) { - log.error("Unable to load lwm2m model [{}]", path.toString()); - throw new RuntimeException("Unable to load lwm2m model", e); + throw new DataValidationException(String.format("Could not parse the XML of objectModel with name %s", path.toString())); } } ); } } - - Path jksPath = Paths.get(getDataDir(), CREDENTIALS_DIR, "serverKeyStore.jks"); - try { - Resource resource = new Resource(); - resource.setTenantId(TenantId.SYS_TENANT_ID); - resource.setResourceType(ResourceType.JKS); - resource.setResourceId(jksPath.getFileName().toString()); - resource.setValue(Base64.getEncoder().encodeToString(Files.readAllBytes(jksPath))); - resourceService.saveResource(resource); - } catch (Exception e) { - log.error("Unable to load lwm2m serverKeyStore [{}]", jksPath.toString()); - throw new RuntimeException("Unable to load l2m2m serverKeyStore", e); - } - } - - private String getObjectModelLwm2mValid(byte[] xmlByte, String streamName, DefaultDDFFileValidator ddfValidator) { - try { - DDFFileParser ddfFileParser = new DDFFileParser(ddfValidator); - ObjectModel objectModel = ddfFileParser.parseEx(new ByteArrayInputStream(xmlByte), streamName).get(0); - return objectModel.id + "##" + objectModel.getVersion(); - } catch (IOException | InvalidDDFFileException e) { - log.error("Could not parse the XML file [{}]", streamName, e); - return null; - } - - } - - private void removeFile(Path modelsDir, String nameFile, byte[] fileBytes) { - String path = "/home/nick/Igor_project/thingsboard_ce_3_2_docker/thingsboard/common/transport/lwm2m/src/main/resources/models/"; - File file = new File(path + nameFile); - if (!file.isDirectory()) { - try { - Files.write(Paths.get(path + "server/" + nameFile), fileBytes); - file.delete(); - } catch (IOException e) { - e.printStackTrace(); - } - - } } public void loadDashboards(TenantId tenantId, CustomerId customerId) throws Exception { diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index f66e6e432e..e99ae89780 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -450,23 +450,27 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { log.info("Updating schema ..."); try { + conn.createStatement().execute("CREATE TABLE IF NOT EXISTS resource ( " + + "id uuid NOT NULL CONSTRAINT resource_pkey PRIMARY KEY, " + + "created_time bigint NOT NULL, " + + "tenant_id uuid NOT NULL, " + + "title varchar(255) NOT NULL, " + + "resource_type varchar(32) NOT NULL, " + + "resource_key varchar(255) NOT NULL, " + + "search_text varchar(255), " + + "file_name varchar(255) NOT NULL, " + + "data varchar, " + + "CONSTRAINT resource_unq_key UNIQUE (tenant_id, resource_type, resource_key)" + + ");"); + + conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3003000;"); + installScripts.loadSystemLwm2mResources(); + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.2.2", SCHEMA_UPDATE_SQL); loadSql(schemaUpdateFile, conn); try { conn.createStatement().execute("ALTER TABLE rule_chain ADD COLUMN type varchar(255) DEFAULT 'CORE'"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script - } catch (Exception e) { - } - - conn.createStatement().execute("CREATE TABLE IF NOT EXISTS resource (" + - " tenant_id uuid NOT NULL," + - " resource_type varchar(32) NOT NULL," + - " resource_id varchar(255) NOT NULL," + - " resource_value varchar," + - " CONSTRAINT resource_unq_key UNIQUE (tenant_id, resource_type, resource_id)" + - " );"); - - conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3003000;"); - installScripts.loadSystemLwm2mResources(); + } catch (Exception ignored) {} } catch (Exception e) { log.error("Failed updating schema!!!", e); } diff --git a/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MModelsRepository.java b/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MModelsRepository.java index 228562a039..51ae94ba2e 100644 --- a/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MModelsRepository.java +++ b/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MModelsRepository.java @@ -17,22 +17,13 @@ package org.thingsboard.server.service.lwm2m; import lombok.extern.slf4j.Slf4j; -import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.util.Hex; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; -import org.springframework.data.domain.PageImpl; import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.lwm2m.LwM2mInstance; -import org.thingsboard.server.common.data.lwm2m.LwM2mObject; -import org.thingsboard.server.common.data.lwm2m.LwM2mResource; import org.thingsboard.server.common.data.lwm2m.ServerSecurityConfig; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.transport.lwm2m.LwM2MTransportConfigBootstrap; import org.thingsboard.server.common.transport.lwm2m.LwM2MTransportConfigServer; -import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode; import java.math.BigInteger; @@ -48,16 +39,6 @@ import java.security.spec.ECParameterSpec; import java.security.spec.ECPoint; import java.security.spec.ECPublicKeySpec; import java.security.spec.KeySpec; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Predicate; -import java.util.stream.Collector; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import static org.thingsboard.server.dao.service.Validator.validateId; @Slf4j @Service @@ -73,127 +54,6 @@ public class LwM2MModelsRepository { @Autowired LwM2MTransportConfigBootstrap contextBootStrap; - /** - * @param objectIds - * @param textSearch - * @return list of LwM2mObject - * Filter by Predicate (uses objectIds, if objectIds is null then it uses textSearch, - * if textSearch is null then it uses AllList from List) - */ - public List getLwm2mObjects(int[] objectIds, String textSearch, String sortProperty, String sortOrder) { - if (objectIds == null && textSearch != null && !textSearch.isEmpty()) { - objectIds = getObjectIdFromTextSearch(textSearch); - } - int[] finalObjectIds = objectIds; - return getLwm2mObjects((objectIds != null && objectIds.length > 0 && textSearch != null && !textSearch.isEmpty()) ? - (ObjectModel element) -> IntStream.of(finalObjectIds).anyMatch(x -> x == element.id) || element.name.toLowerCase().contains(textSearch.toLowerCase()) : - (objectIds != null && objectIds.length > 0) ? - (ObjectModel element) -> IntStream.of(finalObjectIds).anyMatch(x -> x == element.id) : - (textSearch != null && !textSearch.isEmpty()) ? - (ObjectModel element) -> element.name.contains(textSearch) : - null, - sortProperty, sortOrder); - } - - /** - * @param predicate - * @return list of LwM2mObject - */ - private List getLwm2mObjects(Predicate predicate, String sortProperty, String sortOrder) { - List lwM2mObjects = new ArrayList<>(); - List listObjects = (predicate == null) ? this.contextServer.getModelsValueCommon() : - contextServer.getModelsValueCommon().stream() - .filter(predicate) - .collect(Collectors.toList()); - - listObjects.forEach(obj -> { - LwM2mObject lwM2mObject = new LwM2mObject(); - lwM2mObject.setId(obj.id); - lwM2mObject.setName(obj.name); - lwM2mObject.setMultiple(obj.multiple); - lwM2mObject.setMandatory(obj.mandatory); - LwM2mInstance instance = new LwM2mInstance(); - instance.setId(0); - List resources = new ArrayList<>(); - obj.resources.forEach((k, v) -> { - if (!v.operations.isExecutable()) { - LwM2mResource resource = new LwM2mResource(k, v.name, false, false, false); - resources.add(resource); - } - }); - instance.setResources(resources.stream().toArray(LwM2mResource[]::new)); - lwM2mObject.setInstances(new LwM2mInstance[]{instance}); - lwM2mObjects.add(lwM2mObject); - }); - return lwM2mObjects.size() > 1 ? this.sortList (lwM2mObjects, sortProperty, sortOrder) : lwM2mObjects; - } - - private List sortList (List lwM2mObjects, String sortProperty, String sortOrder) { - switch (sortProperty) { - case "name": - switch (sortOrder) { - case "ASC": - lwM2mObjects.sort((o1, o2) -> o1.getName().compareTo(o2.getName())); - break; - case "DESC": - lwM2mObjects.stream().sorted(Comparator.comparing(LwM2mObject::getName).reversed()); - break; - } - case "id": - switch (sortOrder) { - case "ASC": - lwM2mObjects.sort((o1, o2) -> Long.compare(o1.getId(), o2.getId())); - break; - case "DESC": - lwM2mObjects.sort((o1, o2) -> Long.compare(o2.getId(), o1.getId())); - } - } - return lwM2mObjects; - } - - /** - * @param tenantId - * @param pageLink - * @return List of LwM2mObject in PageData format - */ - public PageData findDeviceLwm2mObjects(TenantId tenantId, PageLink pageLink) { - log.trace("Executing findDeviceProfileInfos tenantId [{}], pageLink [{}]", tenantId, pageLink); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - Validator.validatePageLink(pageLink); - return this.findLwm2mListObjects(pageLink); - } - - /** - * @param pageLink - * @return List of LwM2mObject in PageData format, filter == TextSearch - * PageNumber = 1, PageSize = List.size() - */ - public PageData findLwm2mListObjects(PageLink pageLink) { - PageImpl page = new PageImpl<>(getLwm2mObjects(getObjectIdFromTextSearch(pageLink.getTextSearch()), - pageLink.getTextSearch(), - pageLink.getSortOrder().getProperty(), - pageLink.getSortOrder().getDirection().name())); - PageData pageData = new PageData<>(page.getContent(), page.getTotalPages(), page.getTotalElements(), page.hasNext()); - return pageData; - } - - /** - * Filter for id Object - * @param textSearch - - * @return - return Object id only first chartAt in textSearch - */ - private int[] getObjectIdFromTextSearch(String textSearch) { - String filtered = null; - if (textSearch !=null && !textSearch.isEmpty()) { - AtomicInteger a = new AtomicInteger(); - filtered = textSearch.chars () - .mapToObj(chr -> (char) chr) - .filter(i -> Character.isDigit(i) && textSearch.charAt(a.getAndIncrement()) == i) - .collect(Collector.of(StringBuilder::new, StringBuilder::append, StringBuilder::append, StringBuilder::toString)); - } - return (filtered != null && !filtered.isEmpty()) ? new int[]{Integer.parseInt(filtered)} : new int[0]; - } - /** * @param securityMode * @param bootstrapServerIs diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index cf4aa21254..5f5fd3df4b 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasName; -import org.thingsboard.server.common.data.Resource; +import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.DeviceId; @@ -251,28 +251,27 @@ public class DefaultTbClusterService implements TbClusterService { } @Override - public void onResourceChange(Resource resource, TbQueueCallback callback) { + public void onResourceChange(TbResource resource, TbQueueCallback callback) { TenantId tenantId = resource.getTenantId(); - log.trace("[{}][{}][{}] Processing change resource", tenantId, resource.getResourceType(), resource.getResourceId()); + log.trace("[{}][{}][{}] Processing change resource", tenantId, resource.getResourceType(), resource.getResourceKey()); TransportProtos.ResourceUpdateMsg resourceUpdateMsg = TransportProtos.ResourceUpdateMsg.newBuilder() .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) .setResourceType(resource.getResourceType().name()) - .setResourceId(resource.getResourceId()) + .setResourceKey(resource.getResourceKey()) .build(); ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceUpdateMsg(resourceUpdateMsg).build(); broadcast(transportMsg, callback); } @Override - public void onResourceDeleted(Resource resource, TbQueueCallback callback) { - TenantId tenantId = resource.getTenantId(); - log.trace("[{}][{}][{}] Processing delete resource", tenantId, resource.getResourceType(), resource.getResourceId()); + public void onResourceDeleted(TbResource resource, TbQueueCallback callback) { + log.trace("[{}] Processing delete resource", resource); TransportProtos.ResourceDeleteMsg resourceUpdateMsg = TransportProtos.ResourceDeleteMsg.newBuilder() - .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) - .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setTenantIdMSB(resource.getTenantId().getId().getMostSignificantBits()) + .setTenantIdLSB(resource.getTenantId().getId().getLeastSignificantBits()) .setResourceType(resource.getResourceType().name()) - .setResourceId(resource.getResourceId()) + .setResourceKey(resource.getResourceKey()) .build(); ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceDeleteMsg(resourceUpdateMsg).build(); broadcast(transportMsg, callback); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java index d91afa63f1..c5848bed58 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java @@ -19,7 +19,7 @@ import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.common.data.Resource; +import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.EdgeId; @@ -74,10 +74,9 @@ public interface TbClusterService { void onDeviceDeleted(Device device, TbQueueCallback callback); - void onResourceChange(Resource resource, TbQueueCallback callback); + void onResourceChange(TbResource resource, TbQueueCallback callback); - void onResourceDeleted(Resource resource, TbQueueCallback callback); + void onResourceDeleted(TbResource resource, TbQueueCallback callback); void onEdgeEventUpdate(TenantId tenantId, EdgeId edgeId); - } diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index 0db56956c6..54d4667903 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -37,6 +37,7 @@ public enum Resource { TENANT_PROFILE(EntityType.TENANT_PROFILE), DEVICE_PROFILE(EntityType.DEVICE_PROFILE), API_USAGE_STATE(EntityType.API_USAGE_STATE), + TB_RESOURCE(EntityType.TB_RESOURCE), EDGE(EntityType.EDGE); private final EntityType entityType; diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java index c00bf2ea6c..f703102e18 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java @@ -38,6 +38,7 @@ public class SysAdminPermissions extends AbstractPermissions { put(Resource.OAUTH2_CONFIGURATION_INFO, PermissionChecker.allowAllPermissionChecker); put(Resource.OAUTH2_CONFIGURATION_TEMPLATE, PermissionChecker.allowAllPermissionChecker); put(Resource.TENANT_PROFILE, PermissionChecker.allowAllPermissionChecker); + put(Resource.TB_RESOURCE, systemEntityPermissionChecker); } private static final PermissionChecker systemEntityPermissionChecker = new PermissionChecker() { diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index 086dcaf6a3..a5ccae2bd9 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -41,6 +41,7 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.WIDGET_TYPE, widgetsPermissionChecker); put(Resource.DEVICE_PROFILE, tenantEntityPermissionChecker); put(Resource.API_USAGE_STATE, tenantEntityPermissionChecker); + put(Resource.TB_RESOURCE, tbResourcePermissionChecker); put(Resource.EDGE, tenantEntityPermissionChecker); } @@ -102,4 +103,19 @@ public class TenantAdminPermissions extends AbstractPermissions { } }; + + private static final PermissionChecker tbResourcePermissionChecker = new PermissionChecker() { + + @Override + public boolean hasPermission(SecurityUser user, Operation operation, EntityId entityId, HasTenantId entity) { + if (entity.getTenantId() == null || entity.getTenantId().isNullUid()) { + return operation == Operation.READ; + } + if (!user.getTenantId().equals(entity.getTenantId())) { + return false; + } + return true; + } + + }; } diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 9110bf07ba..9613d708d2 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -31,7 +31,7 @@ import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.Resource; +import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; @@ -56,7 +56,7 @@ import org.thingsboard.server.dao.device.provision.ProvisionFailedException; import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.device.provision.ProvisionResponse; import org.thingsboard.server.dao.relation.RelationService; -import org.thingsboard.server.dao.resource.ResourceService; +import org.thingsboard.server.dao.resource.TbResourceService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto; @@ -108,7 +108,7 @@ public class DefaultTransportApiService implements TransportApiService { private final TbClusterService tbClusterService; private final DataDecodingEncodingService dataDecodingEncodingService; private final DeviceProvisionService deviceProvisionService; - private final ResourceService resourceService; + private final TbResourceService resourceService; private final ConcurrentMap deviceCreationLocks = new ConcurrentHashMap<>(); @@ -117,7 +117,7 @@ public class DefaultTransportApiService implements TransportApiService { RelationService relationService, DeviceCredentialsService deviceCredentialsService, DeviceStateService deviceStateService, DbCallbackExecutorService dbCallbackExecutorService, TbClusterService tbClusterService, DataDecodingEncodingService dataDecodingEncodingService, - DeviceProvisionService deviceProvisionService, ResourceService resourceService) { + DeviceProvisionService deviceProvisionService, TbResourceService resourceService) { this.deviceProfileCache = deviceProfileCache; this.tenantProfileCache = tenantProfileCache; this.apiUsageStateService = apiUsageStateService; @@ -365,12 +365,12 @@ public class DefaultTransportApiService implements TransportApiService { private ListenableFuture handle(GetResourceRequestMsg requestMsg) { TenantId tenantId = new TenantId(new UUID(requestMsg.getTenantIdMSB(), requestMsg.getTenantIdLSB())); ResourceType resourceType = ResourceType.valueOf(requestMsg.getResourceType()); - String resourceId = requestMsg.getResourceId(); + String resourceKey = requestMsg.getResourceKey(); TransportProtos.GetResourceResponseMsg.Builder builder = TransportProtos.GetResourceResponseMsg.newBuilder(); - Resource resource = resourceService.getResource(tenantId, resourceType, resourceId); + TbResource resource = resourceService.getResource(tenantId, resourceType, resourceKey); if (resource == null && !tenantId.equals(TenantId.SYS_TENANT_ID)) { - resource = resourceService.getResource(TenantId.SYS_TENANT_ID, resourceType, resourceId); + resource = resourceService.getResource(TenantId.SYS_TENANT_ID, resourceType, resourceKey); } if (resource != null) { diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 973ec8220c..e53dd65c28 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -594,6 +594,28 @@ transport: bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}" bind_port: "${COAP_BIND_PORT:5683}" timeout: "${COAP_TIMEOUT:10000}" + dtls: + # Enable/disable DTLS 1.2 support + enabled: "${COAP_DTLS_ENABLED:false}" + # CoAP DTLS bind address + bind_address: "${COAP_DTLS_BIND_ADDRESS:0.0.0.0}" + # CoAP DTLS bind port + bind_port: "${COAP_DTLS_BIND_PORT:5684}" + # Secure mode. Allowed values: NO_AUTH, X509 + mode: "${COAP_DTLS_SECURE_MODE:NO_AUTH}" + # Path to the key store that holds the certificate + key_store: "${COAP_DTLS_KEY_STORE:coapserver.jks}" + # Password used to access the key store + key_store_password: "${COAP_DTLS_KEY_STORE_PASSWORD:server_ks_password}" + # Password used to access the key + key_password: "${COAP_DTLS_KEY_PASSWORD:server_key_password}" + # Key alias + key_alias: "${COAP_DTLS_KEY_ALIAS:serveralias}" + # Skip certificate validity check for client certificates. + skip_validity_check_for_client_cert: "${COAP_DTLS_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" + x509: + dtls_session_inactivity_timeout: "${TB_COAP_X509_DTLS_SESSION_INACTIVITY_TIMEOUT:86400000}" + dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}" # Local LwM2M transport parameters lwm2m: # Enable/disable lvm2m transport protocol. @@ -602,12 +624,9 @@ transport: # send a Confirmable message to the time when an acknowledgement is no longer expected. # DEFAULT_TIMEOUT = 2 * 60 * 1000l; 2 min in ms timeout: "${LWM2M_TIMEOUT:120000}" -# model_path_file: "${LWM2M_MODEL_PATH_FILE:./common/transport/lwm2m/src/main/resources/models/}" - model_path_file: "${LWM2M_MODEL_PATH_FILE:}" recommended_ciphers: "${LWM2M_RECOMMENDED_CIPHERS:false}" recommended_supported_groups: "${LWM2M_RECOMMENDED_SUPPORTED_GROUPS:true}" - request_pool_size: "${LWM2M_REQUEST_POOL_SIZE:100}" - request_error_pool_size: "${LWM2M_REQUEST_ERROR_POOL_SIZE:10}" + response_pool_size: "${LWM2M_RESPONSE_POOL_SIZE:100}" registered_pool_size: "${LWM2M_REGISTERED_POOL_SIZE:10}" update_registered_pool_size: "${LWM2M_UPDATE_REGISTERED_POOL_SIZE:10}" un_registered_pool_size: "${LWM2M_UN_REGISTERED_POOL_SIZE:10}" @@ -616,8 +635,7 @@ transport: # To get helps about files format and how to generate it, see: https://github.com/eclipse/leshan/wiki/Credential-files-format # Create new X509 Certificates: common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_credentials.sh key_store_type: "${LWM2M_KEYSTORE_TYPE:JKS}" - # key_store_type: "${LWM2M_KEYSTORE_TYPE:PKCS12}" -# key_store_path_file: "${KEY_STORE_PATH_FILE:/usr/share/thingsboard/conf/credentials/serverKeyStore.jks}" + # key_store_path_file: "${KEY_STORE_PATH_FILE:/common/transport/lwm2m/src/main/resources/credentials/serverKeyStore.jks" key_store_path_file: "${KEY_STORE_PATH_FILE:}" key_store_password: "${LWM2M_KEYSTORE_PASSWORD_SERVER:server_ks_password}" root_alias: "${LWM2M_SERVER_ROOT_CA:rootca}" @@ -636,24 +654,26 @@ transport: # - Elliptic Curve parameters : [secp256r1 [NIST P-256, X9.62 prime256v1] (1.2.840.10045.3.1.7)] public_x: "${LWM2M_SERVER_PUBLIC_X:05064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f358}" public_y: "${LWM2M_SERVER_PUBLIC_Y:5eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b}" - private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED:308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420dc774b309e547ceb48fee547e104ce201a9c48c449dc5414cd04e7f5cf05f67ba00a06082a8648ce3d030107a1440342000405064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f3585eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b}" # Only Certificate_x509: + private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED:308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420dc774b309e547ceb48fee547e104ce201a9c48c449dc5414cd04e7f5cf05f67ba00a06082a8648ce3d030107a1440342000405064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f3585eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b}" + # Only Certificate_x509: alias: "${LWM2M_KEYSTORE_ALIAS_SERVER:server}" bootstrap: - enable: "${LWM2M_BOOTSTRAP_ENABLED:true}" - id: "${LWM2M_SERVER_ID:111}" + enable: "${LWM2M_ENABLED_BS:true}" + id: "${LWM2M_SERVER_ID_BS:111}" bind_address: "${LWM2M_BIND_ADDRESS_BS:0.0.0.0}" bind_port_no_sec: "${LWM2M_BIND_PORT_NO_SEC_BS:5687}" secure: bind_address_security: "${LWM2M_BIND_ADDRESS_BS:0.0.0.0}" - bind_port_security: "${LWM2M_BIND_PORT_SEC_BS:5688}" + bind_port_security: "${LWM2M_BIND_PORT_SECURITY_BS:5688}" # Only for RPK: Public & Private Key. If the keystore file is missing or not working # - Elliptic Curve parameters : [secp256r1 [NIST P-256, X9.62 prime256v1] (1.2.840.10045.3.1.7)] # - Public Key (Hex): [3059301306072a8648ce3d020106082a8648ce3d030107034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34] # - Private Key (Hex): [308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104205ecafd90caa7be45c42e1f3f32571632b8409e6e6249d7124f4ba56fab3c8083a00a06082a8648ce3d030107a144034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34], public_x: "${LWM2M_SERVER_PUBLIC_X_BS:5017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f91}" public_y: "${LWM2M_SERVER_PUBLIC_Y_BS:3fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34}" - private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED_BS:308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104205ecafd90caa7be45c42e1f3f32571632b8409e6e6249d7124f4ba56fab3c8083a00a06082a8648ce3d030107a144034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34}" # Only Certificate_x509: - alias: "${LWM2M_KEYSTORE_ALIAS_BOOTSTRAP:bootstrap}" + private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED_BS:308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104205ecafd90caa7be45c42e1f3f32571632b8409e6e6249d7124f4ba56fab3c8083a00a06082a8648ce3d030107a144034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34}" + # Only Certificate_x509: + alias: "${LWM2M_KEYSTORE_ALIAS_BS:bootstrap}" # Use redis for Security and Registration stores redis.enabled: "${LWM2M_REDIS_ENABLED:false}" 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 c615a88142..2329e8086c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -467,6 +467,10 @@ public abstract class AbstractWebTest { return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseType); } + protected R doPostWithTypedResponse(String urlTemplate, T content, TypeReference responseType, ResultMatcher resultMatcher, String... params) throws Exception { + return readResponse(doPost(urlTemplate, content, params).andExpect(resultMatcher), responseType); + } + protected T doPostAsync(String urlTemplate, T content, Class responseClass, ResultMatcher resultMatcher, String... params) throws Exception { return readResponse(doPostAsync(urlTemplate, content, DEFAULT_TIMEOUT, params).andExpect(resultMatcher), responseClass); } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseTbResourceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseTbResourceControllerTest.java new file mode 100644 index 0000000000..8fae23881a --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/BaseTbResourceControllerTest.java @@ -0,0 +1,296 @@ +/** + * 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.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.security.Authority; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public abstract class BaseTbResourceControllerTest extends AbstractControllerTest { + + private IdComparator idComparator = new IdComparator<>(); + + private static final String DEFAULT_FILE_NAME = "test.jks"; + + private Tenant savedTenant; + private User tenantAdmin; + + @Before + public void beforeTest() throws Exception { + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = doPost("/api/tenant", tenant, Tenant.class); + Assert.assertNotNull(savedTenant); + + tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + } + + @After + public void afterTest() throws Exception { + loginSysAdmin(); + + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testSaveTbResource() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JKS); + resource.setTitle("My first resource"); + resource.setFileName(DEFAULT_FILE_NAME); + resource.setData("Test Data"); + + TbResource savedResource = save(resource); + + Assert.assertNotNull(savedResource); + Assert.assertNotNull(savedResource.getId()); + Assert.assertTrue(savedResource.getCreatedTime() > 0); + Assert.assertEquals(savedTenant.getId(), savedResource.getTenantId()); + Assert.assertEquals(resource.getTitle(), savedResource.getTitle()); + Assert.assertEquals(DEFAULT_FILE_NAME, savedResource.getFileName()); + Assert.assertEquals(DEFAULT_FILE_NAME, savedResource.getResourceKey()); + Assert.assertEquals(resource.getData(), savedResource.getData()); + + savedResource.setTitle("My new resource"); + + save(savedResource); + + TbResource foundResource = doGet("/api/resource/" + savedResource.getId().getId().toString(), TbResource.class); + Assert.assertEquals(foundResource.getTitle(), savedResource.getTitle()); + } + + @Test + public void testUpdateTbResourceFromDifferentTenant() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JKS); + resource.setTitle("My first resource"); + resource.setFileName(DEFAULT_FILE_NAME); + resource.setData("Test Data"); + + TbResource savedResource = save(resource); + + loginDifferentTenant(); + doPostWithTypedResponse("/api/resource", Collections.singletonList(savedResource), new TypeReference<>(){}, status().isBadRequest()); + deleteDifferentTenant(); + } + + @Test + public void testFindTbResourceById() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JKS); + resource.setTitle("My first resource"); + resource.setFileName(DEFAULT_FILE_NAME); + resource.setData("Test Data"); + + TbResource savedResource = save(resource); + + TbResource foundResource = doGet("/api/resource/" + savedResource.getId().getId().toString(), TbResource.class); + Assert.assertNotNull(foundResource); + Assert.assertEquals(savedResource, foundResource); + } + + @Test + public void testDeleteTbResource() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JKS); + resource.setTitle("My first resource"); + resource.setFileName(DEFAULT_FILE_NAME); + resource.setData("Test Data"); + + TbResource savedResource = save(resource); + + doDelete("/api/resource/" + savedResource.getId().getId().toString()) + .andExpect(status().isOk()); + + doGet("/api/resource/" + savedResource.getId().getId().toString()) + .andExpect(status().isNotFound()); + } + + @Test + public void testFindTenantTbResources() throws Exception { + List resourcesToSave = new ArrayList<>(); + for (int i = 0; i < 173; i++) { + TbResource resource = new TbResource(); + resource.setTitle("Resource" + i); + resource.setResourceType(ResourceType.JKS); + resource.setFileName(i + DEFAULT_FILE_NAME); + resource.setData("Test Data"); + resourcesToSave.add(resource); + } + + List resources =save(resourcesToSave).stream().map(TbResourceInfo::new).collect(Collectors.toList()); + + List loadedResources = new ArrayList<>(); + PageLink pageLink = new PageLink(24); + PageData pageData; + do { + pageData = doGetTypedWithPageLink("/api/resource?", + new TypeReference>() { + }, pageLink); + loadedResources.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(resources, idComparator); + Collections.sort(loadedResources, idComparator); + + Assert.assertEquals(resources, loadedResources); + } + + @Test + public void testFindSystemTbResources() throws Exception { + loginSysAdmin(); + + List resources = new ArrayList<>(); + for (int i = 0; i < 173; i++) { + TbResource resource = new TbResource(); + resource.setTitle("Resource" + i); + resource.setResourceType(ResourceType.JKS); + resource.setFileName(i + DEFAULT_FILE_NAME); + resource.setData("Test Data"); + resources.add(new TbResourceInfo(save(resource))); + } + List loadedResources = new ArrayList<>(); + PageLink pageLink = new PageLink(24); + PageData pageData; + do { + pageData = doGetTypedWithPageLink("/api/resource?", + new TypeReference>() { + }, pageLink); + loadedResources.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(resources, idComparator); + Collections.sort(loadedResources, idComparator); + + Assert.assertEquals(resources, loadedResources); + + for (TbResourceInfo resource : resources) { + doDelete("/api/resource/" + resource.getId().getId().toString()) + .andExpect(status().isOk()); + } + + pageLink = new PageLink(27); + loadedResources.clear(); + do { + pageData = doGetTypedWithPageLink("/api/resource?", + new TypeReference>() { + }, pageLink); + loadedResources.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Assert.assertTrue(loadedResources.isEmpty()); + } + + @Test + public void testFindSystemAndTenantTbResources() throws Exception { + List systemResources = new ArrayList<>(); + List expectedResources = new ArrayList<>(); + for (int i = 0; i < 73; i++) { + TbResource resource = new TbResource(); + resource.setTitle("Resource" + i); + resource.setResourceType(ResourceType.JKS); + resource.setFileName(i + DEFAULT_FILE_NAME); + resource.setData("Test Data"); + expectedResources.add(new TbResourceInfo(save(resource))); + } + + loginSysAdmin(); + + for (int i = 0; i < 173; i++) { + TbResource resource = new TbResource(); + resource.setTitle("Resource" + i); + resource.setResourceType(ResourceType.JKS); + resource.setFileName(i + DEFAULT_FILE_NAME); + resource.setData("Test Data"); + TbResourceInfo savedResource = new TbResourceInfo(save(resource)); + systemResources.add(savedResource); + if (i >= 73) { + expectedResources.add(savedResource); + } + } + + login(tenantAdmin.getEmail(), "testPassword1"); + + List loadedResources = new ArrayList<>(); + PageLink pageLink = new PageLink(24); + PageData pageData; + do { + pageData = doGetTypedWithPageLink("/api/resource?", + new TypeReference>() { + }, pageLink); + loadedResources.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(expectedResources, idComparator); + Collections.sort(loadedResources, idComparator); + + Assert.assertEquals(expectedResources, loadedResources); + + loginSysAdmin(); + + for (TbResourceInfo resource : systemResources) { + doDelete("/api/resource/" + resource.getId().getId().toString()) + .andExpect(status().isOk()); + } + } + + private TbResource save(TbResource tbResource) throws Exception { + return save(Collections.singletonList(tbResource)).get(0); + } + + private List save(List tbResources) throws Exception { + return doPostWithTypedResponse("/api/resource", tbResources, new TypeReference<>(){}); + } +} diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/TbResourceControllerSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/TbResourceControllerSqlTest.java new file mode 100644 index 0000000000..dd2441e8f2 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/sql/TbResourceControllerSqlTest.java @@ -0,0 +1,23 @@ +/** + * 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.controller.sql; + +import org.thingsboard.server.controller.BaseTbResourceControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class TbResourceControllerSqlTest extends BaseTbResourceControllerTest { +} diff --git a/common/coap-server/pom.xml b/common/coap-server/pom.xml new file mode 100644 index 0000000000..8adbe4329a --- /dev/null +++ b/common/coap-server/pom.xml @@ -0,0 +1,73 @@ + + + + 4.0.0 + + org.thingsboard + 3.3.0-SNAPSHOT + common + + org.thingsboard.common + coap-server + jar + + Thingsboard CoAP server + https://thingsboard.io + + + UTF-8 + ${basedir}/../.. + + + + + org.thingsboard.common + queue + + + org.thingsboard.common + data + + + org.thingsboard.common.transport + transport-api + + + org.springframework + spring-context + + + org.springframework.boot + spring-boot-starter-web + provided + + + org.eclipse.californium + californium-core + + + org.eclipse.californium + scandium + + + + + \ No newline at end of file diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerContext.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerContext.java new file mode 100644 index 0000000000..4129108ba3 --- /dev/null +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerContext.java @@ -0,0 +1,46 @@ +/** + * 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.coapserver; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Component; + +@Slf4j +@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.coap.enabled}'=='true')") +@Component +public class CoapServerContext { + + @Getter + @Value("${transport.coap.bind_address}") + private String host; + + @Getter + @Value("${transport.coap.bind_port}") + private Integer port; + + @Getter + @Value("${transport.coap.timeout}") + private Long timeout; + + @Getter + @Autowired(required = false) + private TbCoapDtlsSettings dtlsSettings; + +} diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerService.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerService.java new file mode 100644 index 0000000000..f8b3ffefc2 --- /dev/null +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/CoapServerService.java @@ -0,0 +1,31 @@ +/** + * 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.coapserver; + +import org.eclipse.californium.core.CoapServer; + +import java.net.UnknownHostException; +import java.util.concurrent.ConcurrentMap; + +public interface CoapServerService { + + CoapServer getCoapServer() throws UnknownHostException; + + ConcurrentMap getDtlsSessionsMap(); + + long getTimeout(); + +} diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java new file mode 100644 index 0000000000..aebac1a86d --- /dev/null +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java @@ -0,0 +1,133 @@ +/** + * 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.coapserver; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.CoapServer; +import org.eclipse.californium.core.network.CoapEndpoint; +import org.eclipse.californium.core.network.config.NetworkConfig; +import org.eclipse.californium.core.server.resources.Resource; +import org.eclipse.californium.scandium.DTLSConnector; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.Random; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.coap.enabled}'=='true')") +public class DefaultCoapServerService implements CoapServerService { + + @Autowired + private CoapServerContext coapServerContext; + + private CoapServer server; + + private TbCoapDtlsCertificateVerifier tbDtlsCertificateVerifier; + + private ScheduledExecutorService dtlsSessionsExecutor; + + @PostConstruct + public void init() throws UnknownHostException { + createCoapServer(); + } + + @PreDestroy + public void shutdown() { + if (dtlsSessionsExecutor != null) { + dtlsSessionsExecutor.shutdownNow(); + } + log.info("Stopping CoAP server!"); + server.destroy(); + log.info("CoAP server stopped!"); + } + + @Override + public CoapServer getCoapServer() throws UnknownHostException { + if (server != null) { + return server; + } else { + return createCoapServer(); + } + } + + @Override + public ConcurrentMap getDtlsSessionsMap() { + return tbDtlsCertificateVerifier != null ? tbDtlsCertificateVerifier.getTbCoapDtlsSessionIdsMap() : null; + } + + @Override + public long getTimeout() { + return coapServerContext.getTimeout(); + } + + private CoapServer createCoapServer() throws UnknownHostException { + server = new CoapServer(); + + CoapEndpoint.Builder noSecCoapEndpointBuilder = new CoapEndpoint.Builder(); + InetAddress addr = InetAddress.getByName(coapServerContext.getHost()); + InetSocketAddress sockAddr = new InetSocketAddress(addr, coapServerContext.getPort()); + noSecCoapEndpointBuilder.setInetSocketAddress(sockAddr); + noSecCoapEndpointBuilder.setNetworkConfig(NetworkConfig.getStandard()); + CoapEndpoint noSecCoapEndpoint = noSecCoapEndpointBuilder.build(); + server.addEndpoint(noSecCoapEndpoint); + + if (isDtlsEnabled()) { + CoapEndpoint.Builder dtlsCoapEndpointBuilder = new CoapEndpoint.Builder(); + TbCoapDtlsSettings dtlsSettings = coapServerContext.getDtlsSettings(); + DtlsConnectorConfig dtlsConnectorConfig = dtlsSettings.dtlsConnectorConfig(); + DTLSConnector connector = new DTLSConnector(dtlsConnectorConfig); + dtlsCoapEndpointBuilder.setConnector(connector); + CoapEndpoint dtlsCoapEndpoint = dtlsCoapEndpointBuilder.build(); + server.addEndpoint(dtlsCoapEndpoint); + if (dtlsConnectorConfig.isClientAuthenticationRequired()) { + tbDtlsCertificateVerifier = (TbCoapDtlsCertificateVerifier) dtlsConnectorConfig.getAdvancedCertificateVerifier(); + dtlsSessionsExecutor = Executors.newSingleThreadScheduledExecutor(); + dtlsSessionsExecutor.scheduleAtFixedRate(this::evictTimeoutSessions, new Random().nextInt((int) getDtlsSessionReportTimeout()), getDtlsSessionReportTimeout(), TimeUnit.MILLISECONDS); + } + } + Resource root = server.getRoot(); + TbCoapServerMessageDeliverer messageDeliverer = new TbCoapServerMessageDeliverer(root); + server.setMessageDeliverer(messageDeliverer); + + server.start(); + return server; + } + + private boolean isDtlsEnabled() { + return coapServerContext.getDtlsSettings() != null; + } + + private void evictTimeoutSessions() { + tbDtlsCertificateVerifier.evictTimeoutSessions(); + } + + private long getDtlsSessionReportTimeout() { + return tbDtlsCertificateVerifier.getDtlsSessionReportTimeout(); + } + +} 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 new file mode 100644 index 0000000000..99a35d4831 --- /dev/null +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsCertificateVerifier.java @@ -0,0 +1,161 @@ +/** + * 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.coapserver; + +import lombok.Data; +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.util.ServerNames; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceTransportType; +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.SessionInfoCreator; +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.queue.discovery.TbServiceInfoProvider; + +import javax.security.auth.x500.X500Principal; +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.Collections; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Data +public class TbCoapDtlsCertificateVerifier implements NewAdvancedCertificateVerifier { + + private final TbCoapDtlsSessionInMemoryStorage tbCoapDtlsSessionInMemoryStorage; + + private TransportService transportService; + private TbServiceInfoProvider serviceInfoProvider; + private boolean skipValidityCheckForClientCert; + + public TbCoapDtlsCertificateVerifier(TransportService transportService, TbServiceInfoProvider serviceInfoProvider, long dtlsSessionInactivityTimeout, long dtlsSessionReportTimeout, boolean skipValidityCheckForClientCert) { + this.transportService = transportService; + this.serviceInfoProvider = serviceInfoProvider; + this.skipValidityCheckForClientCert = skipValidityCheckForClientCert; + this.tbCoapDtlsSessionInMemoryStorage = new TbCoapDtlsSessionInMemoryStorage(dtlsSessionInactivityTimeout, dtlsSessionReportTimeout); + } + + @Override + public List getSupportedCertificateType() { + return Collections.singletonList(CertificateType.X_509); + } + + @Override + public CertificateVerificationResult verifyCertificate(ConnectionId cid, ServerNames serverName, Boolean clientUsage, boolean truncateCertificatePath, CertificateMessage message, DTLSSession session) { + try { + String credentialsBody = null; + 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(DeviceTransportType.COAP, TransportProtos.ValidateDeviceX509CertRequestMsg.newBuilder().setHash(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(); + } + }); + latch.await(10, TimeUnit.SECONDS); + ValidateDeviceCredentialsResponse msg = deviceCredentialsResponse[0]; + if (msg != null && strCert.equals(msg.getCredentials())) { + credentialsBody = msg.getCredentials(); + DeviceProfile deviceProfile = msg.getDeviceProfile(); + if (msg.hasDeviceInfo() && deviceProfile != null) { + TransportProtos.SessionInfoProto sessionInfoProto = SessionInfoCreator.create(msg, serviceInfoProvider.getServiceId(), UUID.randomUUID()); + tbCoapDtlsSessionInMemoryStorage.put(session.getSessionIdentifier().toString(), new TbCoapDtlsSessionInfo(sessionInfoProto, deviceProfile)); + } + break; + } + } catch (InterruptedException | + CertificateEncodingException | + CertificateExpiredException | + CertificateNotYetValidException e) { + log.error(e.getMessage(), e); + } + } + if (credentialsBody == null) { + AlertMessage alert = new AlertMessage(AlertMessage.AlertLevel.FATAL, AlertMessage.AlertDescription.BAD_CERTIFICATE, + session.getPeer()); + throw new HandshakeException("Certificate chain could not be validated", alert); + } else { + 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) { + // empty implementation + } + + public ConcurrentMap getTbCoapDtlsSessionIdsMap() { + return tbCoapDtlsSessionInMemoryStorage.getDtlsSessionIdMap(); + } + + public void evictTimeoutSessions() { + tbCoapDtlsSessionInMemoryStorage.evictTimeoutSessions(); + } + + public long getDtlsSessionReportTimeout() { + return tbCoapDtlsSessionInMemoryStorage.getDtlsSessionReportTimeout(); + } +} \ No newline at end of file diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSessionInMemoryStorage.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSessionInMemoryStorage.java new file mode 100644 index 0000000000..618a10a1eb --- /dev/null +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSessionInMemoryStorage.java @@ -0,0 +1,55 @@ +/** + * 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.coapserver; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +@Slf4j +@Data +public class TbCoapDtlsSessionInMemoryStorage { + + private final ConcurrentMap dtlsSessionIdMap = new ConcurrentHashMap<>(); + private long dtlsSessionInactivityTimeout; + private long dtlsSessionReportTimeout; + + + public TbCoapDtlsSessionInMemoryStorage(long dtlsSessionInactivityTimeout, long dtlsSessionReportTimeout) { + this.dtlsSessionInactivityTimeout = dtlsSessionInactivityTimeout; + this.dtlsSessionReportTimeout = dtlsSessionReportTimeout; + } + + public void put(String dtlsSessionId, TbCoapDtlsSessionInfo dtlsSessionInfo) { + log.trace("DTLS session added to in-memory store: [{}] timestamp: [{}]", dtlsSessionId, dtlsSessionInfo.getLastActivityTime()); + dtlsSessionIdMap.putIfAbsent(dtlsSessionId, dtlsSessionInfo); + } + + public void evictTimeoutSessions() { + long expTime = System.currentTimeMillis() - dtlsSessionInactivityTimeout; + dtlsSessionIdMap.entrySet().removeIf(entry -> { + if (entry.getValue().getLastActivityTime() < expTime) { + log.trace("DTLS session was removed from in-memory store: [{}]", entry.getKey()); + return true; + } else { + return false; + } + }); + } + +} \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Resource.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSessionInfo.java similarity index 50% rename from common/data/src/main/java/org/thingsboard/server/common/data/Resource.java rename to common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSessionInfo.java index 2e7cde8185..893ca38a5c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Resource.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSessionInfo.java @@ -13,29 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data; +package org.thingsboard.server.coapserver; import lombok.Data; -import org.thingsboard.server.common.data.id.TenantId; - -import java.io.Serializable; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.gen.transport.TransportProtos; @Data -public class Resource implements HasTenantId, Serializable { +public class TbCoapDtlsSessionInfo { - private static final long serialVersionUID = 7379609705527272306L; + private TransportProtos.SessionInfoProto sessionInfoProto; + private DeviceProfile deviceProfile; + private long lastActivityTime; - private TenantId tenantId; - private ResourceType resourceType; - private String resourceId; - private String value; - @Override - public String toString() { - return "Resource{" + - "tenantId=" + tenantId + - ", resourceType=" + resourceType + - ", resourceId='" + resourceId + '\'' + - '}'; + public TbCoapDtlsSessionInfo(TransportProtos.SessionInfoProto sessionInfoProto, DeviceProfile deviceProfile) { + this.sessionInfoProto = sessionInfoProto; + this.deviceProfile = deviceProfile; + this.lastActivityTime = System.currentTimeMillis(); } -} +} \ No newline at end of file diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java new file mode 100644 index 0000000000..417a78da12 --- /dev/null +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java @@ -0,0 +1,162 @@ +/** + * 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.coapserver; + +import com.google.common.io.Resources; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.elements.util.SslContextUtil; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.eclipse.californium.scandium.dtls.CertificateType; +import org.eclipse.californium.scandium.dtls.x509.StaticNewAdvancedCertificateVerifier; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.security.GeneralSecurityException; +import java.security.cert.Certificate; +import java.util.Collections; +import java.util.Optional; + +@Slf4j +@ConditionalOnExpression("'${transport.coap.enabled}'=='true'") +@ConditionalOnProperty(prefix = "transport.coap.dtls", value = "enabled", havingValue = "true", matchIfMissing = false) +@Component +public class TbCoapDtlsSettings { + + @Value("${transport.coap.dtls.bind_address}") + private String host; + + @Value("${transport.coap.dtls.bind_port}") + private Integer port; + + @Value("${transport.coap.dtls.mode}") + private String mode; + + @Value("${transport.coap.dtls.key_store}") + private String keyStoreFile; + + @Value("${transport.coap.dtls.key_store_password}") + private String keyStorePassword; + + @Value("${transport.coap.dtls.key_password}") + private String keyPassword; + + @Value("${transport.coap.dtls.key_alias}") + private String keyAlias; + + @Value("${transport.coap.dtls.skip_validity_check_for_client_cert}") + private boolean skipValidityCheckForClientCert; + + @Value("${transport.coap.dtls.x509.dtls_session_inactivity_timeout}") + private long dtlsSessionInactivityTimeout; + + @Value("${transport.coap.dtls.x509.dtls_session_report_timeout}") + private long dtlsSessionReportTimeout; + + @Autowired + private TransportService transportService; + + @Autowired + private TbServiceInfoProvider serviceInfoProvider; + + public DtlsConnectorConfig dtlsConnectorConfig() throws UnknownHostException { + Optional securityModeOpt = SecurityMode.parse(mode); + if (securityModeOpt.isEmpty()) { + log.warn("Incorrect configuration of securityMode {}", mode); + throw new RuntimeException("Failed to parse mode property: " + mode + "!"); + } else { + DtlsConnectorConfig.Builder configBuilder = new DtlsConnectorConfig.Builder(); + configBuilder.setAddress(getInetSocketAddress()); + String keyStoreFilePath = Resources.getResource(keyStoreFile).getPath(); + SslContextUtil.Credentials serverCredentials = loadServerCredentials(keyStoreFilePath); + SecurityMode securityMode = securityModeOpt.get(); + if (securityMode.equals(SecurityMode.NO_AUTH)) { + configBuilder.setClientAuthenticationRequired(false); + configBuilder.setServerOnly(true); + } else { + configBuilder.setAdvancedCertificateVerifier( + new TbCoapDtlsCertificateVerifier( + transportService, + serviceInfoProvider, + dtlsSessionInactivityTimeout, + dtlsSessionReportTimeout, + skipValidityCheckForClientCert + ) + ); + } + configBuilder.setIdentity(serverCredentials.getPrivateKey(), serverCredentials.getCertificateChain(), + Collections.singletonList(CertificateType.X_509)); + return configBuilder.build(); + } + } + + private SslContextUtil.Credentials loadServerCredentials(String keyStoreFilePath) { + try { + return SslContextUtil.loadCredentials(keyStoreFilePath, keyAlias, keyStorePassword.toCharArray(), + keyPassword.toCharArray()); + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException("Failed to load serverCredentials due to: ", e); + } + } + + private void loadTrustedCertificates(DtlsConnectorConfig.Builder config, String keyStoreFilePath) { + StaticNewAdvancedCertificateVerifier.Builder trustBuilder = StaticNewAdvancedCertificateVerifier.builder(); + try { + Certificate[] trustedCertificates = SslContextUtil.loadTrustedCertificates( + keyStoreFilePath, keyAlias, + keyStorePassword.toCharArray()); + trustBuilder.setTrustedCertificates(trustedCertificates); + if (trustBuilder.hasTrusts()) { + config.setAdvancedCertificateVerifier(trustBuilder.build()); + } + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException("Failed to load trusted certificates due to: ", e); + } + } + + private InetSocketAddress getInetSocketAddress() throws UnknownHostException { + InetAddress addr = InetAddress.getByName(host); + return new InetSocketAddress(addr, port); + } + + private enum SecurityMode { + X509, + NO_AUTH; + + static Optional parse(String name) { + SecurityMode mode = null; + if (name != null) { + for (SecurityMode securityMode : SecurityMode.values()) { + if (securityMode.name().equalsIgnoreCase(name)) { + mode = securityMode; + break; + } + } + } + return Optional.ofNullable(mode); + } + + } + +} \ No newline at end of file diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapServerMessageDeliverer.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapServerMessageDeliverer.java similarity index 97% rename from common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapServerMessageDeliverer.java rename to common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapServerMessageDeliverer.java index fa189e35f1..307455a6a5 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapServerMessageDeliverer.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapServerMessageDeliverer.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.transport.coap; +package org.thingsboard.server.coapserver; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.coap.OptionSet; diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesService.java index 1a5b39f078..4a0315b43e 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesService.java @@ -21,8 +21,8 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.device.claim.ClaimResult; +import org.thingsboard.server.dao.device.claim.ReclaimResult; -import java.util.List; import java.util.concurrent.ExecutionException; public interface ClaimDevicesService { @@ -31,6 +31,6 @@ public interface ClaimDevicesService { ListenableFuture claimDevice(Device device, CustomerId customerId, String secretKey) throws ExecutionException, InterruptedException; - ListenableFuture> reClaimDevice(TenantId tenantId, Device device); + ListenableFuture reClaimDevice(TenantId tenantId, Device device); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/transport/resource/Resource.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ReclaimResult.java similarity index 52% rename from common/data/src/main/java/org/thingsboard/server/common/data/transport/resource/Resource.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ReclaimResult.java index c29b704b04..5157afbd3e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/transport/resource/Resource.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ReclaimResult.java @@ -13,25 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.transport.resource; +package org.thingsboard.server.dao.device.claim; +import lombok.AllArgsConstructor; import lombok.Data; -import org.thingsboard.server.common.data.HasTenantId; -import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.Customer; @Data -public class Resource implements HasTenantId { - private TenantId tenantId; - private ResourceType resourceType; - private String resourceId; - private String value; - - @Override - public String toString() { - return "Resource{" + - "tenantId=" + tenantId + - ", resourceType=" + resourceType + - ", resourceId='" + resourceId + '\'' + - '}'; - } +@AllArgsConstructor +public class ReclaimResult { + Customer unassignedCustomer; } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceService.java new file mode 100644 index 0000000000..0093b43dfe --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceService.java @@ -0,0 +1,58 @@ +/** + * 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.dao.resource; + +import org.eclipse.leshan.core.model.InvalidDDFFileException; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.id.TbResourceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.lwm2m.LwM2mObject; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; + +import java.io.IOException; +import java.util.List; + + +public interface TbResourceService { + TbResource saveResource(TbResource resource) throws InvalidDDFFileException, IOException; + + TbResource getResource(TenantId tenantId, ResourceType resourceType, String resourceId); + + TbResource findResourceById(TenantId tenantId, TbResourceId resourceId); + + TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId); + + PageData findAllTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink); + + PageData findTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink); + + List findLwM2mObject(TenantId tenantId, + String sortOrder, + String sortProperty, + String[] objectIds); + + List findLwM2mObjectPage(TenantId tenantId, + String sortProperty, + String sortOrder, + PageLink pageLink); + + void deleteResource(TenantId tenantId, TbResourceId resourceId); + + void deleteResourcesByTenantId(TenantId tenantId); +} diff --git a/common/data/pom.xml b/common/data/pom.xml index 79b3bc8e26..92efc053a9 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -36,6 +36,14 @@ + + javax.validation + validation-api + + + org.owasp.antisamy + antisamy + org.slf4j slf4j-api @@ -79,6 +87,10 @@ org.thingsboard protobuf-dynamic + + org.eclipse.leshan + leshan-core + diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/AdminSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/AdminSettings.java index 9389a2f3d6..356af5dab7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/AdminSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/AdminSettings.java @@ -18,11 +18,13 @@ package org.thingsboard.server.common.data; import org.thingsboard.server.common.data.id.AdminSettingsId; import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.common.data.validation.NoXss; public class AdminSettings extends BaseData { private static final long serialVersionUID = -7670322981725511892L; - + + @NoXss private String key; private transient JsonNode jsonValue; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java index 9af8ddb736..a333591e53 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java @@ -17,19 +17,28 @@ package org.thingsboard.server.common.data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.validation.NoXss; @EqualsAndHashCode(callSuper = true) public abstract class ContactBased extends SearchTextBasedWithAdditionalInfo implements HasName { private static final long serialVersionUID = 5047448057830660988L; - + + @NoXss protected String country; + @NoXss protected String state; + @NoXss protected String city; + @NoXss protected String address; + @NoXss protected String address2; + @NoXss protected String zip; + @NoXss protected String phone; + @NoXss protected String email; public ContactBased() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java index e40ab84925..f6f49bb33b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java @@ -20,13 +20,13 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty.Access; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; - -import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.common.data.validation.NoXss; public class Customer extends ContactBased implements HasTenantId { private static final long serialVersionUID = -1599722990298929275L; - + + @NoXss private String title; private TenantId tenantId; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java index ad93983cac..2b5f9a9c1d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.NoXss; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -36,8 +37,11 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen private TenantId tenantId; private CustomerId customerId; + @NoXss private String name; + @NoXss private String type; + @NoXss private String label; private DeviceProfileId deviceProfileId; private transient DeviceData deviceData; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java index 8b24cefa71..44c1c4b0ac 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java @@ -24,7 +24,9 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.NoXss; +import javax.validation.Valid; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -36,17 +38,22 @@ import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalIn public class DeviceProfile extends SearchTextBased implements HasName, HasTenantId { private TenantId tenantId; + @NoXss private String name; + @NoXss private String description; private boolean isDefault; private DeviceProfileType type; private DeviceTransportType transportType; private DeviceProfileProvisionType provisionType; private RuleChainId defaultRuleChainId; + @NoXss private String defaultQueueName; + @Valid private transient DeviceProfileData profileData; @JsonIgnore private byte[] profileDataBytes; + @NoXss private String provisionDeviceKey; public DeviceProfile() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index 2def7658c5..1090a0d20f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -19,5 +19,5 @@ package org.thingsboard.server.common.data; * @author Andrew Shvayka */ public enum EntityType { - TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE, TENANT_PROFILE, DEVICE_PROFILE, API_USAGE_STATE, EDGE; + TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE, TENANT_PROFILE, DEVICE_PROFILE, API_USAGE_STATE, TB_RESOURCE, EDGE; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java index 55ec23a9b3..e8b48ee23a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.objects.TelemetryEntityView; +import org.thingsboard.server.common.data.validation.NoXss; /** * Created by Victor Basanets on 8/27/2017. @@ -39,7 +40,9 @@ public class EntityView extends SearchTextBasedWithAdditionalInfo private EntityId entityId; private TenantId tenantId; private CustomerId customerId; + @NoXss private String name; + @NoXss private String type; private TelemetryEntityView keys; private long startTimeMs; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java new file mode 100644 index 0000000000..33e94b2e0c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java @@ -0,0 +1,73 @@ +/** + * 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; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.id.TbResourceId; + +@Slf4j +@Data +@EqualsAndHashCode(callSuper = true) +public class TbResource extends TbResourceInfo { + + private static final long serialVersionUID = 7379609705527272306L; + + private String fileName; + + private String data; + + public TbResource() { + super(); + } + + public TbResource(TbResourceId id) { + super(id); + } + + public TbResource(TbResourceInfo resourceInfo) { + super(resourceInfo); + } + + public TbResource(TbResource resource) { + super(resource); + this.data = resource.getData(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Resource [tenantId="); + builder.append(getTenantId()); + builder.append(", id="); + builder.append(getUuidId()); + builder.append(", createdTime="); + builder.append(createdTime); + builder.append(", title="); + builder.append(getTitle()); + builder.append(", resourceType="); + builder.append(getResourceType()); + builder.append(", resourceKey="); + builder.append(getResourceKey()); + builder.append(", fileName="); + builder.append(fileName); + builder.append(", data="); + builder.append(data); + builder.append("]"); + return builder.toString(); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java new file mode 100644 index 0000000000..d72982aeca --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java @@ -0,0 +1,75 @@ +/** + * 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; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.id.TbResourceId; +import org.thingsboard.server.common.data.id.TenantId; + +@Slf4j +@Data +@EqualsAndHashCode(callSuper = true) +public class TbResourceInfo extends SearchTextBased implements HasTenantId { + + private TenantId tenantId; + private String title; + private ResourceType resourceType; + private String resourceKey; + private String searchText; + + public TbResourceInfo() { + super(); + } + + public TbResourceInfo(TbResourceId id) { + super(id); + } + + public TbResourceInfo(TbResourceInfo resourceInfo) { + super(resourceInfo); + this.tenantId = resourceInfo.getTenantId(); + this.title = resourceInfo.getTitle(); + this.resourceType = resourceInfo.getResourceType(); + this.resourceKey = resourceInfo.getResourceKey(); + this.searchText = resourceInfo.getSearchText(); + } + + @Override + public String getSearchText() { + return searchText != null ? searchText : title; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("ResourceInfo [tenantId="); + builder.append(tenantId); + builder.append(", id="); + builder.append(getUuidId()); + builder.append(", createdTime="); + builder.append(createdTime); + builder.append(", title="); + builder.append(title); + builder.append(", resourceType="); + builder.append(resourceType); + builder.append(", resourceKey="); + builder.append(resourceKey); + builder.append("]"); + return builder.toString(); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java index 41dc37ec72..b6adf6cf65 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java @@ -20,13 +20,16 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.validation.NoXss; @EqualsAndHashCode(callSuper = true) public class Tenant extends ContactBased implements HasTenantId { private static final long serialVersionUID = 8057243243859922101L; - + + @NoXss private String title; + @NoXss private String region; private TenantProfileId tenantProfileId; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java index 3796ec856d..a0fefea6cc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java @@ -23,6 +23,7 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.common.data.validation.NoXss; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -34,7 +35,9 @@ import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalIn @Slf4j public class TenantProfile extends SearchTextBased implements HasName { + @NoXss private String name; + @NoXss private String description; private boolean isDefault; private boolean isolatedTbCore; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/User.java b/common/data/src/main/java/org/thingsboard/server/common/data/User.java index 5792d23887..420aff71ce 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/User.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/User.java @@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; -import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.common.data.validation.NoXss; @EqualsAndHashCode(callSuper = true) public class User extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId { @@ -35,7 +35,9 @@ public class User extends SearchTextBasedWithAdditionalInfo implements H private CustomerId customerId; private String email; private Authority authority; + @NoXss private String firstName; + @NoXss private String lastName; public User() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java index 1e789e9811..f9d64cb712 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java @@ -16,10 +16,14 @@ package org.thingsboard.server.common.data.asset; import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.*; +import org.thingsboard.server.common.data.HasCustomerId; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.NoXss; @EqualsAndHashCode(callSuper = true) public class Asset extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId { @@ -28,8 +32,11 @@ public class Asset extends SearchTextBasedWithAdditionalInfo implements private TenantId tenantId; private CustomerId customerId; + @NoXss private String name; + @NoXss private String type; + @NoXss private String label; public Asset() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java index 49004073d3..90de622fb0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java @@ -17,15 +17,16 @@ package org.thingsboard.server.common.data.device.profile; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; -import org.thingsboard.server.common.data.query.KeyFilter; import java.io.Serializable; +import javax.validation.Valid; import java.util.List; @Data @JsonIgnoreProperties(ignoreUnknown = true) public class AlarmCondition implements Serializable { + @Valid private List condition; private AlarmConditionSpec spec; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java index e13db85ed0..96fbd3465f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java @@ -18,15 +18,21 @@ package org.thingsboard.server.common.data.device.profile; import lombok.Data; import org.thingsboard.server.common.data.query.EntityKeyValueType; import org.thingsboard.server.common.data.query.KeyFilterPredicate; +import org.thingsboard.server.common.data.validation.NoXss; + +import javax.validation.Valid; import java.io.Serializable; @Data public class AlarmConditionFilter implements Serializable { + @Valid private AlarmConditionFilterKey key; private EntityKeyValueType valueType; + @NoXss private Object value; + @Valid private KeyFilterPredicate predicate; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java index af7603a74e..258e50a6a0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.device.profile; import lombok.Data; +import org.thingsboard.server.common.data.validation.NoXss; import java.io.Serializable; @@ -23,6 +24,7 @@ import java.io.Serializable; public class AlarmConditionFilterKey implements Serializable { private final AlarmConditionKeyType type; + @NoXss private final String key; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java index a87799eb16..09c9f084cb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java @@ -16,15 +16,20 @@ package org.thingsboard.server.common.data.device.profile; import lombok.Data; +import org.thingsboard.server.common.data.validation.NoXss; + +import javax.validation.Valid; import java.io.Serializable; @Data public class AlarmRule implements Serializable { + @Valid private AlarmCondition condition; private AlarmSchedule schedule; // Advanced + @NoXss private String alarmDetails; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java index 341adb06be..7a99ccb41e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java @@ -17,8 +17,10 @@ package org.thingsboard.server.common.data.device.profile; import lombok.Data; import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.validation.NoXss; import java.io.Serializable; +import javax.validation.Valid; import java.util.List; import java.util.TreeMap; @@ -26,9 +28,12 @@ import java.util.TreeMap; public class DeviceProfileAlarm implements Serializable { private String id; + @NoXss private String alarmType; + @Valid private TreeMap createRules; + @Valid private AlarmRule clearRule; // Hidden in advanced settings diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java index bf290b5bc8..f5a438470e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.device.profile; import lombok.Data; import java.io.Serializable; +import javax.validation.Valid; import java.util.List; @Data @@ -26,6 +27,7 @@ public class DeviceProfileData implements Serializable { private DeviceProfileConfiguration configuration; private DeviceProfileTransportConfiguration transportConfiguration; private DeviceProfileProvisionConfiguration provisionConfiguration; + @Valid private List alarms; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index 5922ff1a53..d7333b4e87 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -69,6 +69,8 @@ public class EntityIdFactory { return new TenantProfileId(uuid); case API_USAGE_STATE: return new ApiUsageStateId(uuid); + case TB_RESOURCE: + return new TbResourceId(uuid); case EDGE: return new EdgeId(uuid); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java new file mode 100644 index 0000000000..566b62db66 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java @@ -0,0 +1,39 @@ +/** + * 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.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.thingsboard.server.common.data.EntityType; + +import java.util.UUID; + +public class TbResourceId extends UUIDBased implements EntityId { + + private static final long serialVersionUID = 1L; + + @JsonCreator + public TbResourceId(@JsonProperty("id") UUID id) { + super(id); + } + + @JsonIgnore + @Override + public EntityType getEntityType() { + return EntityType.TB_RESOURCE; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mConstants.java new file mode 100644 index 0000000000..6170fd4bb3 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mConstants.java @@ -0,0 +1,23 @@ +/** + * 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.lwm2m; + +public interface LwM2mConstants { + + String LWM2M_SEPARATOR_PATH = "/"; + String LWM2M_SEPARATOR_KEY = "_"; + String LWM2M_SEPARATOR_SEARCH_TEXT = ":"; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mInstance.java b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mInstance.java index aeff342582..1e0ff70e8a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mInstance.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mInstance.java @@ -20,6 +20,6 @@ import lombok.Data; @Data public class LwM2mInstance { int id; - LwM2mResource [] resources; + LwM2mResourceObserve[] resources; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mObject.java b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mObject.java index 6401e8a31a..80174b430f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mObject.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mObject.java @@ -20,6 +20,7 @@ import lombok.Data; @Data public class LwM2mObject { int id; + String keyId; String name; boolean multiple; boolean mandatory; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mResource.java b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mResourceObserve.java similarity index 92% rename from common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mResource.java rename to common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mResourceObserve.java index 9317232b9d..402309ebf3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mResource.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mResourceObserve.java @@ -22,7 +22,7 @@ import java.util.stream.Stream; @Data @AllArgsConstructor -public class LwM2mResource { +public class LwM2mResourceObserve { int id; String name; boolean observe; @@ -30,7 +30,7 @@ public class LwM2mResource { boolean telemetry; String keyName; - public LwM2mResource(int id, String name, boolean observe, boolean attribute, boolean telemetry) { + public LwM2mResourceObserve(int id, String name, boolean observe, boolean attribute, boolean telemetry) { this.id = id; this.name = name; this.observe = observe; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java index 6f6a7746b7..307447ef9c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.query; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import lombok.RequiredArgsConstructor; +import org.thingsboard.server.common.data.validation.NoXss; import java.io.Serializable; @@ -29,6 +30,7 @@ public class DynamicValue implements Serializable { private T resolvedValue; private final DynamicValueSourceType sourceType; + @NoXss private final String sourceAttribute; private final boolean inherit; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/FilterPredicateValue.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/FilterPredicateValue.java index 6865c47bd4..aedf516096 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/FilterPredicateValue.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/FilterPredicateValue.java @@ -20,6 +20,9 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.Getter; +import org.thingsboard.server.common.data.validation.NoXss; + +import javax.validation.Valid; import java.io.Serializable; @@ -27,10 +30,13 @@ import java.io.Serializable; public class FilterPredicateValue implements Serializable { @Getter + @NoXss private final T defaultValue; @Getter + @NoXss private final T userValue; @Getter + @Valid private final DynamicValue dynamicValue; public FilterPredicateValue(T defaultValue) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/StringFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/StringFilterPredicate.java index fffe38cd57..d3a09813e3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/StringFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/StringFilterPredicate.java @@ -17,10 +17,13 @@ package org.thingsboard.server.common.data.query; import lombok.Data; +import javax.validation.Valid; + @Data public class StringFilterPredicate implements SimpleKeyFilterPredicate { private StringOperation operation; + @Valid private FilterPredicateValue value; private boolean ignoreCase; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java index 39157c4709..da84f15427 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.NoXss; @Data @EqualsAndHashCode(callSuper = true) @@ -35,6 +36,7 @@ public class RuleChain extends SearchTextBasedWithAdditionalInfo im private static final long serialVersionUID = -5656679015121935465L; private TenantId tenantId; + @NoXss private String name; private RuleChainType type; private RuleNodeId firstRuleNodeId; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/validation/NoXss.java b/common/data/src/main/java/org/thingsboard/server/common/data/validation/NoXss.java new file mode 100644 index 0000000000..2ecc737fee --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/validation/NoXss.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.validation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@Constraint(validatedBy = {}) +public @interface NoXss { + String message() default "field value is malformed"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/common/pom.xml b/common/pom.xml index 84739924b9..892f4fb582 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -43,6 +43,7 @@ dao-api stats cache + coap-server edge-api diff --git a/common/queue/src/main/proto/queue.proto b/common/queue/src/main/proto/queue.proto index cf08fc4e75..2150992bbb 100644 --- a/common/queue/src/main/proto/queue.proto +++ b/common/queue/src/main/proto/queue.proto @@ -205,7 +205,7 @@ message GetResourceRequestMsg { int64 tenantIdMSB = 1; int64 tenantIdLSB = 2; string resourceType = 3; - string resourceId = 4; + string resourceKey = 4; } message GetResourceResponseMsg { @@ -257,14 +257,14 @@ message ResourceUpdateMsg { int64 tenantIdMSB = 1; int64 tenantIdLSB = 2; string resourceType = 3; - string resourceId = 4; + string resourceKey = 4; } message ResourceDeleteMsg { int64 tenantIdMSB = 1; int64 tenantIdLSB = 2; string resourceType = 3; - string resourceId = 4; + string resourceKey = 4; } message SessionCloseNotificationProto { diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index 958f20337f..2fefb8f300 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -40,10 +40,18 @@ org.thingsboard.common.transport transport-api + + org.thingsboard.common + coap-server + org.eclipse.californium californium-core + + org.eclipse.californium + scandium + org.springframework spring-context-support diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java index 8cd117e99c..9133809225 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java @@ -18,13 +18,12 @@ package org.thingsboard.server.transport.coap; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; import org.thingsboard.server.common.transport.TransportContext; -import org.thingsboard.server.transport.coap.efento.adaptor.EfentoCoapAdaptor; import org.thingsboard.server.transport.coap.adaptors.JsonCoapAdaptor; import org.thingsboard.server.transport.coap.adaptors.ProtoCoapAdaptor; +import org.thingsboard.server.transport.coap.efento.adaptor.EfentoCoapAdaptor; /** @@ -35,18 +34,6 @@ import org.thingsboard.server.transport.coap.adaptors.ProtoCoapAdaptor; @Component public class CoapTransportContext extends TransportContext { - @Getter - @Value("${transport.coap.bind_address}") - private String host; - - @Getter - @Value("${transport.coap.bind_port}") - private Integer port; - - @Getter - @Value("${transport.coap.timeout}") - private Long timeout; - @Getter @Autowired private JsonCoapAdaptor jsonCoapAdaptor; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java index d91b1dc214..8d756631e8 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java @@ -27,6 +27,9 @@ import org.eclipse.californium.core.observe.ObserveRelation; import org.eclipse.californium.core.server.resources.CoapExchange; import org.eclipse.californium.core.server.resources.Resource; import org.eclipse.californium.core.server.resources.ResourceObserver; +import org.springframework.util.StringUtils; +import org.thingsboard.server.coapserver.CoapServerService; +import org.thingsboard.server.coapserver.TbCoapDtlsSessionInfo; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceTransportType; @@ -53,11 +56,8 @@ import java.util.List; import java.util.Optional; import java.util.Set; import java.util.UUID; -import java.util.Timer; -import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.atomic.AtomicInteger; @Slf4j @@ -66,15 +66,24 @@ public class CoapTransportResource extends AbstractCoapTransportResource { private static final int FEATURE_TYPE_POSITION = 4; private static final int REQUEST_ID_POSITION = 5; + private static final int FEATURE_TYPE_POSITION_CERTIFICATE_REQUEST = 3; + private static final int REQUEST_ID_POSITION_CERTIFICATE_REQUEST = 4; + private static final String DTLS_SESSION_ID_KEY = "DTLS_SESSION_ID"; + private final ConcurrentMap tokenToSessionIdMap = new ConcurrentHashMap<>(); private final ConcurrentMap tokenToNotificationCounterMap = new ConcurrentHashMap<>(); private final Set rpcSubscriptions = ConcurrentHashMap.newKeySet(); private final Set attributeSubscriptions = ConcurrentHashMap.newKeySet(); - public CoapTransportResource(CoapTransportContext coapTransportContext, String name) { + private ConcurrentMap dtlsSessionIdMap; + private long timeout; + + public CoapTransportResource(CoapTransportContext coapTransportContext, CoapServerService coapServerService, String name) { super(coapTransportContext, name); this.setObservable(true); // enable observing this.addObserver(new CoapResourceObserver()); + this.dtlsSessionIdMap = coapServerService.getDtlsSessionsMap(); + this.timeout = coapServerService.getTimeout(); // this.setObservable(false); // disable observing // this.setObserveType(CoAP.Type.CON); // configure the notification type to CONs // this.getAttributes().setObservable(); // mark observable in the Link-Format @@ -190,111 +199,135 @@ public class CoapTransportResource extends AbstractCoapTransportResource { Exchange advanced = exchange.advanced(); Request request = advanced.getRequest(); + String dtlsSessionIdStr = request.getSourceContext().get(DTLS_SESSION_ID_KEY); + if (!StringUtils.isEmpty(dtlsSessionIdStr)) { + if (dtlsSessionIdMap != null) { + TbCoapDtlsSessionInfo tbCoapDtlsSessionInfo = dtlsSessionIdMap + .computeIfPresent(dtlsSessionIdStr, (dtlsSessionId, dtlsSessionInfo) -> { + dtlsSessionInfo.setLastActivityTime(System.currentTimeMillis()); + return dtlsSessionInfo; + }); + if (tbCoapDtlsSessionInfo != null) { + processRequest(exchange, type, request, tbCoapDtlsSessionInfo.getSessionInfoProto(), tbCoapDtlsSessionInfo.getDeviceProfile()); + } else { + exchange.respond(CoAP.ResponseCode.UNAUTHORIZED); + } + } else { + processAccessTokenRequest(exchange, type, request); + } + } else { + processAccessTokenRequest(exchange, type, request); + } + } + + private void processAccessTokenRequest(CoapExchange exchange, SessionMsgType type, Request request) { Optional credentials = decodeCredentials(request); if (credentials.isEmpty()) { - exchange.respond(CoAP.ResponseCode.BAD_REQUEST); + exchange.respond(CoAP.ResponseCode.UNAUTHORIZED); return; } - transportService.process(DeviceTransportType.COAP, TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(credentials.get().getCredentialsId()).build(), new CoapDeviceAuthCallback(transportContext, exchange, (sessionInfo, deviceProfile) -> { - UUID sessionId = new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB()); - try { - TransportConfigurationContainer transportConfigurationContainer = getTransportConfigurationContainer(deviceProfile); - CoapTransportAdaptor coapTransportAdaptor = getCoapTransportAdaptor(transportConfigurationContainer.isJsonPayload()); - switch (type) { - case POST_ATTRIBUTES_REQUEST: - transportService.process(sessionInfo, - coapTransportAdaptor.convertToPostAttributes(sessionId, request, - transportConfigurationContainer.getAttributesMsgDescriptor()), - new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); - reportActivity(sessionInfo, attributeSubscriptions.contains(sessionId), rpcSubscriptions.contains(sessionId)); - break; - case POST_TELEMETRY_REQUEST: - transportService.process(sessionInfo, - coapTransportAdaptor.convertToPostTelemetry(sessionId, request, - transportConfigurationContainer.getTelemetryMsgDescriptor()), - new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); - reportActivity(sessionInfo, attributeSubscriptions.contains(sessionId), rpcSubscriptions.contains(sessionId)); - break; - case CLAIM_REQUEST: - transportService.process(sessionInfo, - coapTransportAdaptor.convertToClaimDevice(sessionId, request, sessionInfo), - new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); - break; - case SUBSCRIBE_ATTRIBUTES_REQUEST: - TransportProtos.SessionInfoProto currentAttrSession = tokenToSessionIdMap.get(getTokenFromRequest(request)); - if (currentAttrSession == null) { - attributeSubscriptions.add(sessionId); - registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, getTokenFromRequest(request)); - transportService.process(sessionInfo, - TransportProtos.SubscribeToAttributeUpdatesMsg.getDefaultInstance(), new CoapNoOpCallback(exchange)); - } - break; - case UNSUBSCRIBE_ATTRIBUTES_REQUEST: - TransportProtos.SessionInfoProto attrSession = lookupAsyncSessionInfo(getTokenFromRequest(request)); - if (attrSession != null) { - UUID attrSessionId = new UUID(attrSession.getSessionIdMSB(), attrSession.getSessionIdLSB()); - attributeSubscriptions.remove(attrSessionId); - transportService.process(attrSession, - TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().setUnsubscribe(true).build(), - new CoapOkCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); - closeAndDeregister(sessionInfo, sessionId); - } - break; - case SUBSCRIBE_RPC_COMMANDS_REQUEST: - TransportProtos.SessionInfoProto currentRpcSession = tokenToSessionIdMap.get(getTokenFromRequest(request)); - if (currentRpcSession == null) { - rpcSubscriptions.add(sessionId); - registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, getTokenFromRequest(request)); - transportService.process(sessionInfo, - TransportProtos.SubscribeToRPCMsg.getDefaultInstance(), - new CoapNoOpCallback(exchange)); - } else { - UUID rpcSessionId = new UUID(currentRpcSession.getSessionIdMSB(), currentRpcSession.getSessionIdLSB()); - reportActivity(currentRpcSession, attributeSubscriptions.contains(rpcSessionId), rpcSubscriptions.contains(rpcSessionId)); - } - break; - case UNSUBSCRIBE_RPC_COMMANDS_REQUEST: - TransportProtos.SessionInfoProto rpcSession = lookupAsyncSessionInfo(getTokenFromRequest(request)); - if (rpcSession != null) { - UUID rpcSessionId = new UUID(rpcSession.getSessionIdMSB(), rpcSession.getSessionIdLSB()); - rpcSubscriptions.remove(rpcSessionId); - transportService.process(rpcSession, - TransportProtos.SubscribeToRPCMsg.newBuilder().setUnsubscribe(true).build(), - new CoapOkCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); - closeAndDeregister(sessionInfo, sessionId); - } - break; - case TO_DEVICE_RPC_RESPONSE: - transportService.process(sessionInfo, - coapTransportAdaptor.convertToDeviceRpcResponse(sessionId, request), - new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); - break; - case TO_SERVER_RPC_REQUEST: - transportService.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor), transportContext.getTimeout()); - transportService.process(sessionInfo, - coapTransportAdaptor.convertToServerRpcRequest(sessionId, request), - new CoapNoOpCallback(exchange)); - break; - case GET_ATTRIBUTES_REQUEST: - transportService.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor), transportContext.getTimeout()); - transportService.process(sessionInfo, - coapTransportAdaptor.convertToGetAttributes(sessionId, request), - new CoapNoOpCallback(exchange)); - break; - } - } catch (AdaptorException e) { - log.trace("[{}] Failed to decode message: ", sessionId, e); - exchange.respond(CoAP.ResponseCode.BAD_REQUEST); - } + processRequest(exchange, type, request, sessionInfo, deviceProfile); })); } + private void processRequest(CoapExchange exchange, SessionMsgType type, Request request, TransportProtos.SessionInfoProto sessionInfo, DeviceProfile deviceProfile) { + UUID sessionId = new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB()); + try { + TransportConfigurationContainer transportConfigurationContainer = getTransportConfigurationContainer(deviceProfile); + CoapTransportAdaptor coapTransportAdaptor = getCoapTransportAdaptor(transportConfigurationContainer.isJsonPayload()); + switch (type) { + case POST_ATTRIBUTES_REQUEST: + transportService.process(sessionInfo, + coapTransportAdaptor.convertToPostAttributes(sessionId, request, + transportConfigurationContainer.getAttributesMsgDescriptor()), + new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + reportActivity(sessionInfo, attributeSubscriptions.contains(sessionId), rpcSubscriptions.contains(sessionId)); + break; + case POST_TELEMETRY_REQUEST: + transportService.process(sessionInfo, + coapTransportAdaptor.convertToPostTelemetry(sessionId, request, + transportConfigurationContainer.getTelemetryMsgDescriptor()), + new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + reportActivity(sessionInfo, attributeSubscriptions.contains(sessionId), rpcSubscriptions.contains(sessionId)); + break; + case CLAIM_REQUEST: + transportService.process(sessionInfo, + coapTransportAdaptor.convertToClaimDevice(sessionId, request, sessionInfo), + new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + break; + case SUBSCRIBE_ATTRIBUTES_REQUEST: + TransportProtos.SessionInfoProto currentAttrSession = tokenToSessionIdMap.get(getTokenFromRequest(request)); + if (currentAttrSession == null) { + attributeSubscriptions.add(sessionId); + registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, getTokenFromRequest(request)); + transportService.process(sessionInfo, + TransportProtos.SubscribeToAttributeUpdatesMsg.getDefaultInstance(), new CoapNoOpCallback(exchange)); + } + break; + case UNSUBSCRIBE_ATTRIBUTES_REQUEST: + TransportProtos.SessionInfoProto attrSession = lookupAsyncSessionInfo(getTokenFromRequest(request)); + if (attrSession != null) { + UUID attrSessionId = new UUID(attrSession.getSessionIdMSB(), attrSession.getSessionIdLSB()); + attributeSubscriptions.remove(attrSessionId); + transportService.process(attrSession, + TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().setUnsubscribe(true).build(), + new CoapOkCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + closeAndDeregister(sessionInfo, sessionId); + } + break; + case SUBSCRIBE_RPC_COMMANDS_REQUEST: + TransportProtos.SessionInfoProto currentRpcSession = tokenToSessionIdMap.get(getTokenFromRequest(request)); + if (currentRpcSession == null) { + rpcSubscriptions.add(sessionId); + registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, getTokenFromRequest(request)); + transportService.process(sessionInfo, + TransportProtos.SubscribeToRPCMsg.getDefaultInstance(), + new CoapNoOpCallback(exchange)); + } else { + UUID rpcSessionId = new UUID(currentRpcSession.getSessionIdMSB(), currentRpcSession.getSessionIdLSB()); + reportActivity(currentRpcSession, attributeSubscriptions.contains(rpcSessionId), rpcSubscriptions.contains(rpcSessionId)); + } + break; + case UNSUBSCRIBE_RPC_COMMANDS_REQUEST: + TransportProtos.SessionInfoProto rpcSession = lookupAsyncSessionInfo(getTokenFromRequest(request)); + if (rpcSession != null) { + UUID rpcSessionId = new UUID(rpcSession.getSessionIdMSB(), rpcSession.getSessionIdLSB()); + rpcSubscriptions.remove(rpcSessionId); + transportService.process(rpcSession, + TransportProtos.SubscribeToRPCMsg.newBuilder().setUnsubscribe(true).build(), + new CoapOkCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + closeAndDeregister(sessionInfo, sessionId); + } + break; + case TO_DEVICE_RPC_RESPONSE: + transportService.process(sessionInfo, + coapTransportAdaptor.convertToDeviceRpcResponse(sessionId, request), + new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + break; + case TO_SERVER_RPC_REQUEST: + transportService.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor), timeout); + transportService.process(sessionInfo, + coapTransportAdaptor.convertToServerRpcRequest(sessionId, request), + new CoapNoOpCallback(exchange)); + break; + case GET_ATTRIBUTES_REQUEST: + transportService.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor), timeout); + transportService.process(sessionInfo, + coapTransportAdaptor.convertToGetAttributes(sessionId, request), + new CoapNoOpCallback(exchange)); + break; + } + } catch (AdaptorException e) { + log.trace("[{}] Failed to decode message: ", sessionId, e); + exchange.respond(CoAP.ResponseCode.BAD_REQUEST); + } + } + private TransportProtos.SessionInfoProto lookupAsyncSessionInfo(String token) { tokenToNotificationCounterMap.remove(token); return tokenToSessionIdMap.remove(token); - } private void registerAsyncCoapSession(CoapExchange exchange, TransportProtos.SessionInfoProto sessionInfo, CoapTransportAdaptor coapTransportAdaptor, String token) { @@ -314,7 +347,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource { private Optional decodeCredentials(Request request) { List uriPath = request.getOptions().getUriPath(); - if (uriPath.size() >= ACCESS_TOKEN_POSITION) { + if (uriPath.size() > ACCESS_TOKEN_POSITION) { return Optional.of(new DeviceTokenCredentials(uriPath.get(ACCESS_TOKEN_POSITION - 1))); } else { return Optional.empty(); @@ -326,8 +359,11 @@ public class CoapTransportResource extends AbstractCoapTransportResource { try { if (uriPath.size() >= FEATURE_TYPE_POSITION) { return Optional.of(FeatureType.valueOf(uriPath.get(FEATURE_TYPE_POSITION - 1).toUpperCase())); - } else if (uriPath.size() == 3 && uriPath.contains(DataConstants.PROVISION)) { - return Optional.of(FeatureType.valueOf(DataConstants.PROVISION.toUpperCase())); + } else if (uriPath.size() >= FEATURE_TYPE_POSITION_CERTIFICATE_REQUEST) { + if (uriPath.contains(DataConstants.PROVISION)) { + return Optional.of(FeatureType.valueOf(DataConstants.PROVISION.toUpperCase())); + } + return Optional.of(FeatureType.valueOf(uriPath.get(FEATURE_TYPE_POSITION_CERTIFICATE_REQUEST - 1).toUpperCase())); } } catch (RuntimeException e) { log.warn("Failed to decode feature type: {}", uriPath); @@ -340,6 +376,8 @@ public class CoapTransportResource extends AbstractCoapTransportResource { try { if (uriPath.size() >= REQUEST_ID_POSITION) { return Optional.of(Integer.valueOf(uriPath.get(REQUEST_ID_POSITION - 1))); + } else { + return Optional.of(Integer.valueOf(uriPath.get(REQUEST_ID_POSITION_CERTIFICATE_REQUEST - 1))); } } catch (RuntimeException e) { log.warn("Failed to decode feature type: {}", uriPath); diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java index de242fc7dd..3de8816246 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java @@ -24,12 +24,11 @@ import org.eclipse.californium.core.server.resources.Resource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; +import org.thingsboard.server.coapserver.CoapServerService; import org.thingsboard.server.transport.coap.efento.CoapEfentoTransportResource; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.UnknownHostException; @Service("CoapTransportService") @@ -42,50 +41,32 @@ public class CoapTransportService { private static final String EFENTO = "efento"; private static final String MEASUREMENTS = "m"; + @Autowired + private CoapServerService coapServerService; + @Autowired private CoapTransportContext coapTransportContext; - private CoapServer server; + private CoapServer coapServer; @PostConstruct public void init() throws UnknownHostException { log.info("Starting CoAP transport..."); - log.info("Starting CoAP transport server"); - - this.server = new CoapServer(); - createResources(); - Resource root = this.server.getRoot(); - TbCoapServerMessageDeliverer messageDeliverer = new TbCoapServerMessageDeliverer(root); - this.server.setMessageDeliverer(messageDeliverer); - - InetAddress addr = InetAddress.getByName(coapTransportContext.getHost()); - InetSocketAddress sockAddr = new InetSocketAddress(addr, coapTransportContext.getPort()); - - CoapEndpoint.Builder coapEndpoitBuilder = new CoapEndpoint.Builder(); - coapEndpoitBuilder.setInetSocketAddress(sockAddr); - CoapEndpoint coapEndpoint = coapEndpoitBuilder.build(); - server.addEndpoint(coapEndpoint); - server.start(); - log.info("CoAP transport started!"); - } - - private void createResources() { + coapServer = coapServerService.getCoapServer(); CoapResource api = new CoapResource(API); - api.add(new CoapTransportResource(coapTransportContext, V1)); + api.add(new CoapTransportResource(coapTransportContext, coapServerService, V1)); CoapResource efento = new CoapResource(EFENTO); CoapEfentoTransportResource efentoMeasurementsTransportResource = new CoapEfentoTransportResource(coapTransportContext, MEASUREMENTS); efento.add(efentoMeasurementsTransportResource); - - server.add(api); - server.add(efento); + coapServer.add(api); + coapServer.add(efento); + log.info("CoAP transport started!"); } @PreDestroy public void shutdown() { - log.info("Stopping CoAP transport!"); - this.server.destroy(); log.info("CoAP transport stopped!"); } } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/NoSecClient.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/NoSecClient.java new file mode 100644 index 0000000000..f9a31d0513 --- /dev/null +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/NoSecClient.java @@ -0,0 +1,97 @@ +/** + * 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.coap.client; + +import org.eclipse.californium.core.CoapClient; +import org.eclipse.californium.core.CoapResponse; +import org.eclipse.californium.core.Utils; +import org.eclipse.californium.elements.DtlsEndpointContext; +import org.eclipse.californium.elements.EndpointContext; +import org.eclipse.californium.elements.exception.ConnectorException; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.Principal; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class NoSecClient { + + private ExecutorService executor = Executors.newFixedThreadPool(1); + private CoapClient coapClient; + + public NoSecClient(String host, int port, String accessToken, String clientKeys, String sharedKeys) throws URISyntaxException { + URI uri = new URI(getFutureUrl(host, port, accessToken, clientKeys, sharedKeys)); + this.coapClient = new CoapClient(uri); + } + + public void test() { + executor.submit(() -> { + try { + while (!Thread.interrupted()) { + CoapResponse response = null; + try { + response = coapClient.get(); + } catch (ConnectorException | IOException e) { + System.err.println("Error occurred while sending request: " + e); + System.exit(-1); + } + if (response != null) { + + System.out.println(response.getCode() + " - " + response.getCode().name()); + System.out.println(response.getOptions()); + System.out.println(response.getResponseText()); + System.out.println(); + System.out.println("ADVANCED:"); + EndpointContext context = response.advanced().getSourceContext(); + Principal identity = context.getPeerIdentity(); + if (identity != null) { + System.out.println(context.getPeerIdentity()); + } else { + System.out.println("anonymous"); + } + System.out.println(context.get(DtlsEndpointContext.KEY_CIPHER)); + System.out.println(Utils.prettyPrint(response)); + } else { + System.out.println("No response received."); + } + Thread.sleep(5000); + } + } catch (Exception e) { + System.out.println("Error occurred while sending COAP requests."); + } + }); + } + + private String getFutureUrl(String host, Integer port, String accessToken, String clientKeys, String sharedKeys) { + return "coap://" + host + ":" + port + "/api/v1/" + accessToken + "/attributes?clientKeys=" + clientKeys + "&sharedKeys=" + sharedKeys; + } + + public static void main(String[] args) throws URISyntaxException { + System.out.println("Usage: java -cp ... org.thingsboard.server.transport.coap.client.NoSecClient " + + "host port accessToken clientKeys sharedKeys"); + + String host = args[0]; + int port = Integer.parseInt(args[1]); + String accessToken = args[2]; + String clientKeys = args[3]; + String sharedKeys = args[4]; + + NoSecClient client = new NoSecClient(host, port, accessToken, clientKeys, sharedKeys); + client.test(); + } +} diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/SecureClientNoAuth.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/SecureClientNoAuth.java new file mode 100644 index 0000000000..7bbb1f55cf --- /dev/null +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/SecureClientNoAuth.java @@ -0,0 +1,145 @@ +/** + * 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.coap.client; + +import org.eclipse.californium.core.CoapClient; +import org.eclipse.californium.core.CoapResponse; +import org.eclipse.californium.core.Utils; +import org.eclipse.californium.core.network.CoapEndpoint; +import org.eclipse.californium.elements.DtlsEndpointContext; +import org.eclipse.californium.elements.EndpointContext; +import org.eclipse.californium.elements.exception.ConnectorException; +import org.eclipse.californium.elements.util.SslContextUtil; +import org.eclipse.californium.scandium.DTLSConnector; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.eclipse.californium.scandium.dtls.CertificateType; +import org.eclipse.californium.scandium.dtls.x509.StaticNewAdvancedCertificateVerifier; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.GeneralSecurityException; +import java.security.Principal; +import java.security.cert.Certificate; +import java.util.Collections; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class SecureClientNoAuth { + + private final DTLSConnector dtlsConnector; + private ExecutorService executor = Executors.newFixedThreadPool(1); + private CoapClient coapClient; + + public SecureClientNoAuth(DTLSConnector dtlsConnector, String host, int port, String accessToken, String clientKeys, String sharedKeys) throws URISyntaxException { + this.dtlsConnector = dtlsConnector; + this.coapClient = getCoapClient(host, port, accessToken, clientKeys, sharedKeys); + } + + public void test() { + executor.submit(() -> { + try { + while (!Thread.interrupted()) { + CoapResponse response = null; + try { + response = coapClient.get(); + } catch (ConnectorException | IOException e) { + System.err.println("Error occurred while sending request: " + e); + System.exit(-1); + } + if (response != null) { + + System.out.println(response.getCode() + " - " + response.getCode().name()); + System.out.println(response.getOptions()); + System.out.println(response.getResponseText()); + System.out.println(); + System.out.println("ADVANCED:"); + EndpointContext context = response.advanced().getSourceContext(); + Principal identity = context.getPeerIdentity(); + if (identity != null) { + System.out.println(context.getPeerIdentity()); + } else { + System.out.println("anonymous"); + } + System.out.println(context.get(DtlsEndpointContext.KEY_CIPHER)); + System.out.println(Utils.prettyPrint(response)); + } else { + System.out.println("No response received."); + } + Thread.sleep(5000); + } + } catch (Exception e) { + System.out.println("Error occurred while sending COAP requests."); + } + }); + } + + private CoapClient getCoapClient(String host, Integer port, String accessToken, String clientKeys, String sharedKeys) throws URISyntaxException { + URI uri = new URI(getFutureUrl(host, port, accessToken, clientKeys, sharedKeys)); + CoapClient client = new CoapClient(uri); + CoapEndpoint.Builder builder = new CoapEndpoint.Builder(); + builder.setConnector(dtlsConnector); + + client.setEndpoint(builder.build()); + return client; + } + + private String getFutureUrl(String host, Integer port, String accessToken, String clientKeys, String sharedKeys) { + return "coaps://" + host + ":" + port + "/api/v1/" + accessToken + "/attributes?clientKeys=" + clientKeys + "&sharedKeys=" + sharedKeys; + } + + public static void main(String[] args) throws URISyntaxException { + System.out.println("Usage: java -cp ... org.thingsboard.server.transport.coap.client.SecureClientNoAuth " + + "host port accessToken keyStoreUriPath keyStoreAlias trustedAliasPattern clientKeys sharedKeys"); + + String host = args[0]; + int port = Integer.parseInt(args[1]); + String accessToken = args[2]; + String clientKeys = args[7]; + String sharedKeys = args[8]; + + String keyStoreUriPath = args[3]; + String keyStoreAlias = args[4]; + String trustedAliasPattern = args[5]; + String keyStorePassword = args[6]; + + + DtlsConnectorConfig.Builder builder = new DtlsConnectorConfig.Builder(); + setupCredentials(builder, keyStoreUriPath, keyStoreAlias, trustedAliasPattern, keyStorePassword); + DTLSConnector dtlsConnector = new DTLSConnector(builder.build()); + SecureClientNoAuth client = new SecureClientNoAuth(dtlsConnector, host, port, accessToken, clientKeys, sharedKeys); + client.test(); + } + + private static void setupCredentials(DtlsConnectorConfig.Builder config, String keyStoreUriPath, String keyStoreAlias, String trustedAliasPattern, String keyStorePassword) { + StaticNewAdvancedCertificateVerifier.Builder trustBuilder = StaticNewAdvancedCertificateVerifier.builder(); + try { + SslContextUtil.Credentials serverCredentials = SslContextUtil.loadCredentials( + keyStoreUriPath, keyStoreAlias, keyStorePassword.toCharArray(), keyStorePassword.toCharArray()); + Certificate[] trustedCertificates = SslContextUtil.loadTrustedCertificates( + keyStoreUriPath, trustedAliasPattern, keyStorePassword.toCharArray()); + trustBuilder.setTrustedCertificates(trustedCertificates); + config.setAdvancedCertificateVerifier(trustBuilder.build()); + config.setIdentity(serverCredentials.getPrivateKey(), serverCredentials.getCertificateChain(), Collections.singletonList(CertificateType.X_509)); + } catch (GeneralSecurityException e) { + System.err.println("certificates are invalid!"); + throw new IllegalArgumentException(e.getMessage()); + } catch (IOException e) { + System.err.println("certificates are missing!"); + throw new IllegalArgumentException(e.getMessage()); + } + } +} diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/SecureClientX509.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/SecureClientX509.java new file mode 100644 index 0000000000..31dd628b40 --- /dev/null +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/SecureClientX509.java @@ -0,0 +1,144 @@ +/** + * 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.coap.client; + +import org.eclipse.californium.core.CoapClient; +import org.eclipse.californium.core.CoapResponse; +import org.eclipse.californium.core.Utils; +import org.eclipse.californium.core.network.CoapEndpoint; +import org.eclipse.californium.elements.DtlsEndpointContext; +import org.eclipse.californium.elements.EndpointContext; +import org.eclipse.californium.elements.exception.ConnectorException; +import org.eclipse.californium.elements.util.SslContextUtil; +import org.eclipse.californium.scandium.DTLSConnector; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.eclipse.californium.scandium.dtls.CertificateType; +import org.eclipse.californium.scandium.dtls.x509.StaticNewAdvancedCertificateVerifier; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.GeneralSecurityException; +import java.security.Principal; +import java.security.cert.Certificate; +import java.util.Collections; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class SecureClientX509 { + + private final DTLSConnector dtlsConnector; + private ExecutorService executor = Executors.newFixedThreadPool(1); + private CoapClient coapClient; + + public SecureClientX509(DTLSConnector dtlsConnector, String host, int port, String clientKeys, String sharedKeys) throws URISyntaxException { + this.dtlsConnector = dtlsConnector; + this.coapClient = getCoapClient(host, port, clientKeys, sharedKeys); + } + + public void test() { + executor.submit(() -> { + try { + while (!Thread.interrupted()) { + CoapResponse response = null; + try { + response = coapClient.get(); + } catch (ConnectorException | IOException e) { + System.err.println("Error occurred while sending request: " + e); + System.exit(-1); + } + if (response != null) { + + System.out.println(response.getCode() + " - " + response.getCode().name()); + System.out.println(response.getOptions()); + System.out.println(response.getResponseText()); + System.out.println(); + System.out.println("ADVANCED:"); + EndpointContext context = response.advanced().getSourceContext(); + Principal identity = context.getPeerIdentity(); + if (identity != null) { + System.out.println(context.getPeerIdentity()); + } else { + System.out.println("anonymous"); + } + System.out.println(context.get(DtlsEndpointContext.KEY_CIPHER)); + System.out.println(Utils.prettyPrint(response)); + } else { + System.out.println("No response received."); + } + Thread.sleep(5000); + } + } catch (Exception e) { + System.out.println("Error occurred while sending COAP requests."); + } + }); + } + + private CoapClient getCoapClient(String host, Integer port, String clientKeys, String sharedKeys) throws URISyntaxException { + URI uri = new URI(getFutureUrl(host, port, clientKeys, sharedKeys)); + CoapClient client = new CoapClient(uri); + CoapEndpoint.Builder builder = new CoapEndpoint.Builder(); + builder.setConnector(dtlsConnector); + + client.setEndpoint(builder.build()); + return client; + } + + private String getFutureUrl(String host, Integer port, String clientKeys, String sharedKeys) { + return "coaps://" + host + ":" + port + "/api/v1/attributes?clientKeys=" + clientKeys + "&sharedKeys=" + sharedKeys; + } + + public static void main(String[] args) throws URISyntaxException { + System.out.println("Usage: java -cp ... org.thingsboard.server.transport.coap.client.SecureClientX509 " + + "host port keyStoreUriPath keyStoreAlias trustedAliasPattern clientKeys sharedKeys"); + + String host = args[0]; + int port = Integer.parseInt(args[1]); + String clientKeys = args[6]; + String sharedKeys = args[7]; + + String keyStoreUriPath = args[2]; + String keyStoreAlias = args[3]; + String trustedAliasPattern = args[4]; + String keyStorePassword = args[5]; + + + DtlsConnectorConfig.Builder builder = new DtlsConnectorConfig.Builder(); + setupCredentials(builder, keyStoreUriPath, keyStoreAlias, trustedAliasPattern, keyStorePassword); + DTLSConnector dtlsConnector = new DTLSConnector(builder.build()); + SecureClientX509 client = new SecureClientX509(dtlsConnector, host, port, clientKeys, sharedKeys); + client.test(); + } + + private static void setupCredentials(DtlsConnectorConfig.Builder config, String keyStoreUriPath, String keyStoreAlias, String trustedAliasPattern, String keyStorePassword) { + StaticNewAdvancedCertificateVerifier.Builder trustBuilder = StaticNewAdvancedCertificateVerifier.builder(); + try { + SslContextUtil.Credentials serverCredentials = SslContextUtil.loadCredentials( + keyStoreUriPath, keyStoreAlias, keyStorePassword.toCharArray(), keyStorePassword.toCharArray()); + Certificate[] trustedCertificates = SslContextUtil.loadTrustedCertificates( + keyStoreUriPath, trustedAliasPattern, keyStorePassword.toCharArray()); + trustBuilder.setTrustedCertificates(trustedCertificates); + config.setAdvancedCertificateVerifier(trustBuilder.build()); + config.setIdentity(serverCredentials.getPrivateKey(), serverCredentials.getCertificateChain(), Collections.singletonList(CertificateType.X_509)); + } catch (GeneralSecurityException e) { + System.err.println("certificates are invalid!"); + throw new IllegalArgumentException(e.getMessage()); + } catch (IOException e) { + System.err.println("certificates are missing!"); + throw new IllegalArgumentException(e.getMessage()); + } + } +} diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml index fe19dae329..aae103fc4f 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -40,6 +40,10 @@ org.thingsboard.common.transport transport-api + + org.thingsboard.common + data + org.springframework spring-context-support 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 8e266a2638..ee2b3bac20 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 @@ -166,13 +166,13 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore { 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()); - context.sentParametersOnThingsboard(context.getTelemetryMsgObject(logMsg), LwM2mTransportHandler.DEVICE_TELEMETRY_TOPIC, sessionInfo); + context.sendParametersOnThingsboard(context.getTelemetryMsgObject(logMsg), LwM2mTransportHandler.DEVICE_TELEMETRY_TOPIC, 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()); - context.sentParametersOnThingsboard(context.getTelemetryMsgObject(logMsg), LwM2mTransportHandler.DEVICE_TELEMETRY_TOPIC, sessionInfo); + context.sendParametersOnThingsboard(context.getTelemetryMsgObject(logMsg), LwM2mTransportHandler.DEVICE_TELEMETRY_TOPIC, sessionInfo); return null; } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java index f89f7ec952..ffe2f76602 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java @@ -26,6 +26,8 @@ import org.eclipse.leshan.server.registration.RegistrationUpdate; import java.util.Collection; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertToIdVerFromObjectId; + @Slf4j public class LwM2mServerListener { @@ -90,7 +92,7 @@ public class LwM2mServerListener { public void onResponse(Observation observation, Registration registration, ObserveResponse response) { if (registration != null) { try { - service.onObservationResponse(registration, observation.getPath().toString(), response); + service.onObservationResponse(registration, convertToIdVerFromObjectId(observation.getPath().toString(), registration), response); } catch (Exception e) { log.error("[{}] onResponse", e.toString()); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java index 15fb015f32..20baeba453 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java @@ -20,10 +20,11 @@ import io.netty.util.concurrent.GenericFutureListener; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.transport.SessionMsgListener; import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.SessionCloseNotificationProto; import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcResponseMsg; @@ -85,4 +86,16 @@ public class LwM2mSessionMsgListener implements GenericFutureListener future) throws Exception { log.info("[{}] operationComplete", future); } + + public void onResourceUpdate(Optional resourceUpdateMsgOpt) { + if (ResourceType.LWM2M_MODEL.name().equals(resourceUpdateMsgOpt.get().getResourceType())) { + this.service.onResourceUpdate(resourceUpdateMsgOpt); + } + } + + public void onResourceDelete(Optional resourceDeleteMsgOpt) { + if (ResourceType.LWM2M_MODEL.name().equals(resourceDeleteMsgOpt.get().getResourceType())) { + this.service.onResourceDelete(resourceDeleteMsgOpt); + } + } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportContextServer.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportContextServer.java index 2fffd17a4c..170530f084 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportContextServer.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportContextServer.java @@ -89,12 +89,12 @@ public class LwM2mTransportContextServer extends TransportContext { } /** - * Sent to Thingsboard Attribute || Telemetry + * send to Thingsboard Attribute || Telemetry * * @param msg - JsonObject: [{name: value}] * @return - dummy */ - private TransportServiceCallback getPubAckCallbackSentAttrTelemetry(final T msg) { + private TransportServiceCallback getPubAckCallbackSendAttrTelemetry(final T msg) { return new TransportServiceCallback<>() { @Override public void onSuccess(Void dummy) { @@ -108,16 +108,16 @@ public class LwM2mTransportContextServer extends TransportContext { }; } - public void sentParametersOnThingsboard(JsonElement msg, String topicName, SessionInfoProto sessionInfo) { + public void sendParametersOnThingsboard(JsonElement msg, String topicName, SessionInfoProto sessionInfo) { try { if (topicName.equals(LwM2mTransportHandler.DEVICE_ATTRIBUTES_TOPIC)) { PostAttributeMsg postAttributeMsg = adaptor.convertToPostAttributes(msg); - TransportServiceCallback call = this.getPubAckCallbackSentAttrTelemetry(postAttributeMsg); - transportService.process(sessionInfo, postAttributeMsg, this.getPubAckCallbackSentAttrTelemetry(call)); + TransportServiceCallback call = this.getPubAckCallbackSendAttrTelemetry(postAttributeMsg); + transportService.process(sessionInfo, postAttributeMsg, this.getPubAckCallbackSendAttrTelemetry(call)); } else if (topicName.equals(LwM2mTransportHandler.DEVICE_TELEMETRY_TOPIC)) { PostTelemetryMsg postTelemetryMsg = adaptor.convertToPostTelemetry(msg); - TransportServiceCallback call = this.getPubAckCallbackSentAttrTelemetry(postTelemetryMsg); - transportService.process(sessionInfo, postTelemetryMsg, this.getPubAckCallbackSentAttrTelemetry(call)); + TransportServiceCallback call = this.getPubAckCallbackSendAttrTelemetry(postTelemetryMsg); + transportService.process(sessionInfo, postTelemetryMsg, this.getPubAckCallbackSendAttrTelemetry(call)); } } catch (AdaptorException e) { log.error("[{}] Failed to process publish msg [{}]", topicName, e); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportHandler.java index 5b6ec23a6e..0f0707ddf8 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportHandler.java @@ -22,6 +22,7 @@ import com.google.gson.JsonSyntaxException; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.eclipse.californium.core.network.config.NetworkConfig; +import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.node.LwM2mMultipleResource; import org.eclipse.leshan.core.node.LwM2mNode; @@ -32,6 +33,7 @@ import org.eclipse.leshan.core.node.LwM2mSingleResource; import org.eclipse.leshan.core.node.codec.CodecException; import org.eclipse.leshan.core.util.Hex; import org.eclipse.leshan.server.californium.LeshanServerBuilder; +import org.eclipse.leshan.server.registration.Registration; import org.nustaq.serialization.FSTConfiguration; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; @@ -47,14 +49,12 @@ import java.util.Date; import java.util.LinkedList; import java.util.Optional; +import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_KEY; +import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; + @Slf4j -//@Component("LwM2MTransportHandler") -//@ConditionalOnExpression("('${service.type:null}'=='tb-transport' && '${transport.lwm2m.enabled:false}'=='true' )|| ('${service.type:null}'=='monolith' && '${transport.lwm2m.enabled}'=='true')") public class LwM2mTransportHandler { - // We choose a default timeout a bit higher to the MAX_TRANSMIT_WAIT(62-93s) which is the time from starting to - // send a Confirmable message to the time when an acknowledgement is no longer expected. - public static final String BASE_DEVICE_API_TOPIC = "v1/devices/me"; public static final String ATTRIBUTE = "attribute"; public static final String TELEMETRY = "telemetry"; @@ -84,6 +84,8 @@ public class LwM2mTransportHandler { public static final String LOG_LW2M_ERROR = "error"; public static final String LOG_LW2M_WARN = "warn"; + public static final int LWM2M_STRATEGY_1 = 1; + public static final int LWM2M_STRATEGY_2 = 2; public static final String CLIENT_NOT_AUTHORIZED = "Client not authorized"; @@ -110,39 +112,6 @@ public class LwM2mTransportHandler { public static final String SERVICE_CHANNEL = "SERVICE"; public static final String RESPONSE_CHANNEL = "RESP"; -// @Autowired -// @Qualifier("LeshanServerCert") -// private LeshanServer lhServerCert; -// -// @Autowired -// @Qualifier("LeshanServerNoSecPskRpk") -// private LeshanServer lhServerNoSecPskRpk; - -// @Autowired -// @Qualifier("ServerListenerCert") -// private LwM2mServerListener serverListenerCert; -// -// @Autowired -// @Qualifier("ServerListenerNoSecPskRpk") -// private LwM2mServerListener serverListenerNoSecPskRpk; - - -// @PostConstruct -// public void init() { -// try { -// serverListenerCert.init(lhServerCert); -// this.lhServerCert.getRegistrationService().addListener(serverListenerCert.registrationListener); -// this.lhServerCert.getPresenceService().addListener(serverListenerCert.presenceListener); -// this.lhServerCert.getObservationService().addListener(serverListenerCert.observationListener); -// serverListenerNoSecPskRpk.init(lhServerNoSecPskRpk); -// this.lhServerNoSecPskRpk.getRegistrationService().addListener(serverListenerNoSecPskRpk.registrationListener); -// this.lhServerNoSecPskRpk.getPresenceService().addListener(serverListenerNoSecPskRpk.presenceListener); -// this.lhServerNoSecPskRpk.getObservationService().addListener(serverListenerNoSecPskRpk.observationListener); -// } catch (Exception e) { -// log.error("init [{}]", e.toString()); -// } -// } - public static NetworkConfig getCoapConfig(Integer serverPortNoSec, Integer serverSecurePort) { NetworkConfig coapConfig; File configFile = new File(NetworkConfig.DEFAULT_FILE_NAME); @@ -202,10 +171,10 @@ public class LwM2mTransportHandler { /** * @return deviceProfileBody with Observe&Attribute&Telemetry From Thingsboard - * Example: + * Example: * property: {"clientLwM2mSettings": { - * clientUpdateValueAfterConnect: false; - * } + * clientUpdateValueAfterConnect: false; + * } * property: "observeAttr" * {"keyName": { * "/3/0/1": "modelNumber", @@ -222,7 +191,7 @@ public class LwM2mTransportHandler { try { ObjectMapper mapper = new ObjectMapper(); String profileStr = mapper.writeValueAsString(profile); - JsonObject profileJson = (profileStr != null) ? validateJson(profileStr) : null; + JsonObject profileJson = (profileStr != null) ? validateJson(profileStr) : null; return (getValidateCredentialsBodyFromThingsboard(profileJson)) ? LwM2mTransportHandler.getNewProfileParameters(profileJson, deviceProfile.getTenantId()) : null; } catch (IOException e) { log.error("", e); @@ -246,9 +215,9 @@ public class LwM2mTransportHandler { return null; } - public static boolean getClientOnlyObserveAfterConnect (LwM2mClientProfile profile) { - return profile.getPostClientLwM2mSettings().getAsJsonObject().has("clientOnlyObserveAfterConnect") && - profile.getPostClientLwM2mSettings().getAsJsonObject().get("clientOnlyObserveAfterConnect").getAsBoolean(); + public static int getClientOnlyObserveAfterConnect(LwM2mClientProfile profile) { + return profile.getPostClientLwM2mSettings().getAsJsonObject().has("clientOnlyObserveAfterConnect") ? + profile.getPostClientLwM2mSettings().getAsJsonObject().get("clientOnlyObserveAfterConnect").getAsInt() : 1; } private static boolean getValidateCredentialsBodyFromThingsboard(JsonObject objectMsg) { @@ -346,4 +315,49 @@ public class LwM2mTransportHandler { } }; } + + public static String convertToObjectIdFromIdVer(String key) { + try { + String[] keyArray = key.split(LWM2M_SEPARATOR_PATH); + if (keyArray.length > 1 && keyArray[1].split(LWM2M_SEPARATOR_KEY).length == 2) { + keyArray[1] = keyArray[1].split(LWM2M_SEPARATOR_KEY)[0]; + return StringUtils.join(keyArray, LWM2M_SEPARATOR_PATH); + } else { + return key; + } + } catch (Exception e) { + return null; + } + } + + public static String convertToIdVerFromObjectId(String path, Registration registration) { + String ver = registration.getSupportedObject().get(new LwM2mPath(path).getObjectId()); + try { + String[] keyArray = path.split(LWM2M_SEPARATOR_PATH); + if (keyArray.length > 1) { + keyArray[1] = keyArray[1] + LWM2M_SEPARATOR_KEY + ver; + return StringUtils.join(keyArray, LWM2M_SEPARATOR_PATH); + } else { + return path; + } + } catch (Exception e) { + return null; + } + } + + public static Integer validateObjectIdFromKey(String key) { + try { + return Integer.parseInt(key.split(LWM2M_SEPARATOR_PATH)[1].split(LWM2M_SEPARATOR_KEY)[0]); + } catch (Exception e) { + return null; + } + } + + public static String validateObjectVerFromKey(String key) { + try { + return (key.split(LWM2M_SEPARATOR_PATH)[1].split(LWM2M_SEPARATOR_KEY)[1]); + } catch (Exception e) { + return ObjectModel.DEFAULT_VERSION; + } + } } 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 fa7f0d68b8..ad1e542ce1 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 @@ -72,6 +72,8 @@ import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandle import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.PUT_TYPE_OPER_WRITE_ATTRIBUTES; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.PUT_TYPE_OPER_WRITE_UPDATE; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.RESPONSE_CHANNEL; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertToIdVerFromObjectId; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertToObjectIdFromIdVer; @Slf4j @Service @@ -81,7 +83,7 @@ public class LwM2mTransportRequest { private LwM2mValueConverterImpl converter; - private final LwM2mTransportContextServer context; + private final LwM2mTransportContextServer lwM2mTransportContextServer; private final LwM2mClientContext lwM2mClientContext; @@ -89,8 +91,8 @@ public class LwM2mTransportRequest { private final LwM2mTransportServiceImpl serviceImpl; - public LwM2mTransportRequest(LwM2mTransportContextServer context, LwM2mClientContext lwM2mClientContext, LeshanServer leshanServer, LwM2mTransportServiceImpl serviceImpl) { - this.context = context; + public LwM2mTransportRequest(LwM2mTransportContextServer lwM2mTransportContextServer, LwM2mClientContext lwM2mClientContext, LeshanServer leshanServer, LwM2mTransportServiceImpl serviceImpl) { + this.lwM2mTransportContextServer = lwM2mTransportContextServer; this.lwM2mClientContext = lwM2mClientContext; this.leshanServer = leshanServer; this.serviceImpl = serviceImpl; @@ -99,7 +101,7 @@ public class LwM2mTransportRequest { @PostConstruct public void init() { this.converter = LwM2mValueConverterImpl.getInstance(); - executorResponse = Executors.newFixedThreadPool(this.context.getLwM2MTransportConfigServer().getRequestPoolSize(), + executorResponse = Executors.newFixedThreadPool(this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getResponsePoolSize(), new NamedThreadFactory(String.format("LwM2M %s channel response", RESPONSE_CHANNEL))); } @@ -107,18 +109,20 @@ public class LwM2mTransportRequest { * Device management and service enablement, including Read, Write, Execute, Discover, Create, Delete and Write-Attributes * * @param registration - - * @param target - + * @param targetIdVer - * @param typeOper - * @param contentFormatParam - * @param observation - */ - public void sendAllRequest(Registration registration, String target, String typeOper, + public void sendAllRequest(Registration registration, String targetIdVer, String typeOper, String contentFormatParam, Observation observation, Object params, long timeoutInMs) { + String target = convertToObjectIdFromIdVer(targetIdVer); LwM2mPath resultIds = new LwM2mPath(target); if (registration != null && resultIds.getObjectId() >= 0) { DownlinkRequest request = null; ContentFormat contentFormat = contentFormatParam != null ? ContentFormat.fromName(contentFormatParam.toUpperCase()) : null; - ResourceModel resource = serviceImpl.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getResourceModel(registration, resultIds); + LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClientWithReg(registration, null); + ResourceModel resource = lwM2MClient.getResourceModel(target); timeoutInMs = timeoutInMs > 0 ? timeoutInMs : DEFAULT_TIMEOUT; switch (typeOper) { case GET_TYPE_OPER_READ: @@ -149,11 +153,13 @@ public class LwM2mTransportRequest { case POST_TYPE_OPER_WRITE_REPLACE: // Request to write a String Single-Instance Resource using the TLV content format. if (resource != null && contentFormat != null) { - if (contentFormat.equals(ContentFormat.TLV) && !resource.multiple) { +// if (contentFormat.equals(ContentFormat.TLV) && !resource.multiple) { + if (contentFormat.equals(ContentFormat.TLV)) { request = this.getWriteRequestSingleResource(null, resultIds.getObjectId(), resultIds.getObjectInstanceId(), resultIds.getResourceId(), params, resource.type, registration); } // Mode.REPLACE && Request to write a String Single-Instance Resource using the given content format (TEXT, TLV, JSON) - else if (!contentFormat.equals(ContentFormat.TLV) && !resource.multiple) { +// else if (!contentFormat.equals(ContentFormat.TLV) && !resource.multiple) { + else if (!contentFormat.equals(ContentFormat.TLV)) { request = this.getWriteRequestSingleResource(contentFormat, resultIds.getObjectId(), resultIds.getObjectInstanceId(), resultIds.getResourceId(), params, resource.type, registration); } } @@ -213,7 +219,10 @@ public class LwM2mTransportRequest { } if (request != null) { - this.sendRequest(registration, request, timeoutInMs); + this.sendRequest(registration, lwM2MClient, request, timeoutInMs); + } + else { + log.error("[{}], [{}] - [{}] error SendRequest", registration.getEndpoint(), typeOper, targetIdVer); } } } @@ -226,12 +235,10 @@ public class LwM2mTransportRequest { */ @SuppressWarnings("unchecked") - private void sendRequest(Registration registration, DownlinkRequest request, long timeoutInMs) { - LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClientWithReg(registration, null); + private void sendRequest(Registration registration, LwM2mClient lwM2MClient, DownlinkRequest request, long timeoutInMs) { leshanServer.send(registration, request, timeoutInMs, (ResponseCallback) response -> { - if (!lwM2MClient.isInit()) { - lwM2MClient.initValue(this.serviceImpl, request.getPath().toString()); + lwM2MClient.initValue(this.serviceImpl, convertToIdVerFromObjectId(request.getPath().toString(), registration)); } if (isSuccess(((Response) response.getCoapResponse()).getCode())) { this.handleResponse(registration, request.getPath().toString(), response, request); @@ -239,23 +246,23 @@ public class LwM2mTransportRequest { String msg = String.format("%s: sendRequest Replace: CoapCde - %s Lwm2m code - %d name - %s Resource path - %s value - %s SendRequest to Client", LOG_LW2M_INFO, ((Response) response.getCoapResponse()).getCode(), response.getCode().getCode(), response.getCode().getName(), request.getPath().toString(), ((LwM2mSingleResource) ((WriteRequest) request).getNode()).getValue().toString()); - serviceImpl.sentLogsToThingsboard(msg, registration); + serviceImpl.sendLogsToThingsboard(msg, registration); log.info("[{}] [{}] - [{}] [{}] Update SendRequest[{}]", registration.getEndpoint(), ((Response) response.getCoapResponse()).getCode(), response.getCode(), request.getPath().toString(), ((LwM2mSingleResource) ((WriteRequest) request).getNode()).getValue()); } } else { String msg = String.format("%s: sendRequest: CoapCode - %s Lwm2m code - %d name - %s Resource path - %s SendRequest to Client", LOG_LW2M_ERROR, ((Response) response.getCoapResponse()).getCode(), response.getCode().getCode(), response.getCode().getName(), request.getPath().toString()); - serviceImpl.sentLogsToThingsboard(msg, registration); + serviceImpl.sendLogsToThingsboard(msg, registration); log.error("[{}], [{}] - [{}] [{}] error SendRequest", registration.getEndpoint(), ((Response) response.getCoapResponse()).getCode(), response.getCode(), request.getPath().toString()); } }, e -> { if (!lwM2MClient.isInit()) { - lwM2MClient.initValue(this.serviceImpl, request.getPath().toString()); + lwM2MClient.initValue(this.serviceImpl, convertToIdVerFromObjectId(request.getPath().toString(), registration)); } String msg = String.format("%s: sendRequest: Resource path - %s msg error - %s SendRequest to Client", LOG_LW2M_ERROR, request.getPath().toString(), e.toString()); - serviceImpl.sentLogsToThingsboard(msg, registration); + serviceImpl.sendLogsToThingsboard(msg, registration); log.error("[{}] - [{}] error SendRequest", request.getPath().toString(), e.toString()); }); @@ -287,7 +294,7 @@ 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.sentLogsToThingsboard(msg, registration); + serviceImpl.sendLogsToThingsboard(msg, registration); log.error("Path: [{}] type: [{}] value: [{}] errorMsg: [{}]]", patn, type, value, e.toString()); return null; } @@ -310,21 +317,22 @@ public class LwM2mTransportRequest { * @param response - */ private void sendResponse(Registration registration, String path, LwM2mResponse response, DownlinkRequest request) { + String pathIdVer = convertToIdVerFromObjectId(path, registration); if (response instanceof ReadResponse) { - serviceImpl.onObservationResponse(registration, path, (ReadResponse) response); + serviceImpl.onObservationResponse(registration, pathIdVer, (ReadResponse) response); } else if (response instanceof CancelObservationResponse) { - log.info("[{}] Path [{}] CancelObservationResponse 3_Send", path, response); + log.info("[{}] Path [{}] CancelObservationResponse 3_Send", pathIdVer, response); } else if (response instanceof DeleteResponse) { - log.info("[{}] Path [{}] DeleteResponse 5_Send", path, response); + log.info("[{}] Path [{}] DeleteResponse 5_Send", pathIdVer, response); } else if (response instanceof DiscoverResponse) { - log.info("[{}] Path [{}] DiscoverResponse 6_Send", path, response); + log.info("[{}] Path [{}] DiscoverResponse 6_Send", pathIdVer, response); } else if (response instanceof ExecuteResponse) { - log.info("[{}] Path [{}] ExecuteResponse 7_Send", path, response); + log.info("[{}] Path [{}] ExecuteResponse 7_Send", pathIdVer, response); } else if (response instanceof WriteAttributesResponse) { - log.info("[{}] Path [{}] WriteAttributesResponse 8_Send", path, response); + log.info("[{}] Path [{}] WriteAttributesResponse 8_Send", pathIdVer, response); } else if (response instanceof WriteResponse) { - log.info("[{}] Path [{}] WriteAttributesResponse 9_Send", path, response); - serviceImpl.onWriteResponseOk(registration, path, (WriteRequest) request); + log.info("[{}] Path [{}] WriteAttributesResponse 9_Send", pathIdVer, response); + serviceImpl.onWriteResponseOk(registration, pathIdVer, (WriteRequest) request); } } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportService.java index 8d1aff37b5..fb917f1370 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportService.java @@ -47,6 +47,10 @@ public interface LwM2mTransportService { void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, Optional deviceProfileOpt); + void onResourceUpdate (Optional resourceUpdateMsgOpt); + + void onResourceDelete(Optional resourceDeleteMsgOpt); + void doTrigger(Registration registration, String path); void doDisconnect(TransportProtos.SessionInfoProto sessionInfo); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServiceImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServiceImpl.java index 1af4e8bfd8..f8d2716808 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServiceImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServiceImpl.java @@ -52,7 +52,6 @@ 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 org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientProfile; -import org.thingsboard.server.transport.lwm2m.server.client.ResourceValue; import org.thingsboard.server.transport.lwm2m.server.client.ResultsAnalyzerParameters; import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; @@ -73,9 +72,6 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.stream.Collectors; import static org.thingsboard.server.common.transport.util.JsonUtils.getJsonObject; @@ -88,10 +84,15 @@ import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandle import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.LOG_LW2M_ERROR; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.LOG_LW2M_INFO; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.LOG_LW2M_TELEMETRY; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.LWM2M_STRATEGY_2; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.POST_TYPE_OPER_EXECUTE; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.POST_TYPE_OPER_WRITE_REPLACE; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.SERVICE_CHANNEL; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertToIdVerFromObjectId; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertToObjectIdFromIdVer; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.getAckCallback; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.validateObjectIdFromKey; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.validateObjectVerFromKey; @Slf4j @Service @@ -102,8 +103,6 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { private ExecutorService executorUpdateRegistered; private ExecutorService executorUnRegistered; private LwM2mValueConverterImpl converter; - protected final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); - protected final Lock writeLock = readWriteLock.writeLock(); private final TransportService transportService; @@ -157,15 +156,12 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { if (lwM2MClient != null) { SessionInfoProto sessionInfo = this.getValidateSessionInfo(registration); if (sessionInfo != null) { - lwM2MClient.setDeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB())); - lwM2MClient.setProfileId(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB())); - lwM2MClient.setDeviceName(sessionInfo.getDeviceName()); - lwM2MClient.setDeviceProfileName(sessionInfo.getDeviceType()); + this.initLwM2mClient(lwM2MClient, sessionInfo); transportService.registerAsyncSession(sessionInfo, new LwM2mSessionMsgListener(this, sessionInfo)); transportService.process(sessionInfo, DefaultTransportService.getSessionEventMsg(SessionEvent.OPEN), null); transportService.process(sessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().build(), null); - this.sentLogsToThingsboard(LOG_LW2M_INFO + ": Client create after Registration", registration); this.initLwM2mFromClientValue(registration, lwM2MClient); + this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client create after Registration", registration); } else { log.error("Client: [{}] onRegistered [{}] name [{}] sessionInfo ", registration.getId(), registration.getEndpoint(), null); } @@ -189,6 +185,11 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { SessionInfoProto sessionInfo = this.getValidateSessionInfo(registration); if (sessionInfo != null) { this.checkInactivity(sessionInfo); + LwM2mClient lwM2MClient = this.lwM2mClientContext.getLwM2MClient(sessionInfo); + if (lwM2MClient.getDeviceId() == null && lwM2MClient.getProfileId() == null) { + initLwM2mClient(lwM2MClient, sessionInfo); + } + log.info("Client: [{}] updatedReg [{}] name [{}] profile ", registration.getId(), registration.getEndpoint(), sessionInfo.getDeviceType()); } else { log.error("Client: [{}] updatedReg [{}] name [{}] sessionInfo ", registration.getId(), registration.getEndpoint(), null); @@ -208,7 +209,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { executorUnRegistered.submit(() -> { try { this.setCancelObservations(registration); - this.sentLogsToThingsboard(LOG_LW2M_INFO + ": Client unRegistration", registration); + this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client unRegistration", registration); this.closeClientSession(registration); } catch (Throwable t) { log.error("[{}] endpoint [{}] error Unable un registration.", registration.getEndpoint(), t); @@ -216,6 +217,13 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { }); } + private void initLwM2mClient(LwM2mClient lwM2MClient, SessionInfoProto sessionInfo) { + lwM2MClient.setDeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB())); + lwM2MClient.setProfileId(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB())); + lwM2MClient.setDeviceName(sessionInfo.getDeviceName()); + lwM2MClient.setDeviceProfileName(sessionInfo.getDeviceType()); + } + private void closeClientSession(Registration registration) { SessionInfoProto sessionInfo = this.getValidateSessionInfo(registration); if (sessionInfo != null) { @@ -278,7 +286,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { } /** - * Update - sent request in change value resources in Client + * Update - send request in change value resources in Client * Path to resources from profile equal keyName or from ModelObject equal name * Only for resources: isWritable && isPresent as attribute in profile -> LwM2MClientProfile (format: CamelCase) * Delete - nothing * @@ -290,26 +298,26 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { if (msg.getSharedUpdatedCount() > 0) { JsonElement el = JsonConverter.toJson(msg); el.getAsJsonObject().entrySet().forEach(de -> { - String path = this.getPathAttributeUpdate(sessionInfo, de.getKey()); + String pathIdVer = this.getPathAttributeUpdate(sessionInfo, de.getKey()); String value = de.getValue().getAsString(); LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClient(new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB())); LwM2mClientProfile clientProfile = lwM2mClientContext.getProfile(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB())); - if (path != null && !path.isEmpty() && (this.validatePathInAttrProfile(clientProfile, path) || this.validatePathInTelemetryProfile(clientProfile, path))) { - ResourceModel resourceModel = lwM2mTransportContextServer.getLwM2MTransportConfigServer().getResourceModel(lwM2MClient.getRegistration(), new LwM2mPath(path)); + if (pathIdVer != null && !pathIdVer.isEmpty() && (this.validatePathInAttrProfile(clientProfile, pathIdVer) || this.validatePathInTelemetryProfile(clientProfile, pathIdVer))) { + ResourceModel resourceModel = lwM2MClient.getResourceModel(pathIdVer); if (resourceModel != null && resourceModel.operations.isWritable()) { - lwM2mTransportRequest.sendAllRequest(lwM2MClient.getRegistration(), path, POST_TYPE_OPER_WRITE_REPLACE, + lwM2mTransportRequest.sendAllRequest(lwM2MClient.getRegistration(), pathIdVer, POST_TYPE_OPER_WRITE_REPLACE, ContentFormat.TLV.getName(), null, value, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout()); } else { - log.error("Resource path - [{}] value - [{}] is not Writable and cannot be updated", path, value); + log.error("Resource path - [{}] value - [{}] is not Writable and cannot be updated", pathIdVer, value); String logMsg = String.format("%s: attributeUpdate: Resource path - %s value - %s is not Writable and cannot be updated", - LOG_LW2M_ERROR, path, value); - this.sentLogsToThingsboard(logMsg, lwM2MClient.getRegistration()); + LOG_LW2M_ERROR, pathIdVer, value); + this.sendLogsToThingsboard(logMsg, lwM2MClient.getRegistration()); } } else { log.error("Attribute name - [{}] value - [{}] is not present as attribute in profile and cannot be updated", de.getKey(), value); String logMsg = String.format("%s: attributeUpdate: attribute name - %s value - %s is not present as attribute in profile and cannot be updated", LOG_LW2M_ERROR, de.getKey(), value); - this.sentLogsToThingsboard(logMsg, lwM2MClient.getRegistration()); + this.sendLogsToThingsboard(logMsg, lwM2MClient.getRegistration()); } }); } else if (msg.getSharedDeletedCount() > 0) { @@ -347,8 +355,28 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { } /** - * Trigger Server path = "/1/0/8" * + * @param resourceUpdateMsgOpt - + */ + @Override + public void onResourceUpdate (Optional resourceUpdateMsgOpt) { + String idVer = resourceUpdateMsgOpt.get().getResourceKey(); + lwM2mClientContext.getLwM2mClients().values().stream().forEach(e -> e.updateResourceModel(idVer, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getModelProvider())); + } + + /** + * + * @param resourceDeleteMsgOpt - + */ + @Override + public void onResourceDelete(Optional resourceDeleteMsgOpt) { + String pathIdVer = resourceDeleteMsgOpt.get().getResourceKey(); + lwM2mClientContext.getLwM2mClients().values().stream().forEach(e -> e.deleteResources(pathIdVer, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getModelProvider())); + } + + /** + * Trigger Server path = "/1/0/8" + *

* Trigger bootStrap path = "/1/0/9" - have to implemented on client */ @Override @@ -417,7 +445,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { * @param msg - text msg * @param registration - Id of Registration LwM2M Client */ - public void sentLogsToThingsboard(String msg, Registration registration) { + public void sendLogsToThingsboard(String msg, Registration registration) { if (msg != null) { JsonObject telemetries = new JsonObject(); telemetries.addProperty(LOG_LW2M_TELEMETRY, msg); @@ -428,7 +456,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { /** * // !!! Ok - * Prepare Sent to Thigsboard callback - Attribute or Telemetry + * Prepare send to Thigsboard callback - Attribute or Telemetry * * @param msg - JsonArray: [{name: value}] * @param topicName - Api Attribute or Telemetry @@ -437,7 +465,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { public void updateParametersOnThingsboard(JsonElement msg, String topicName, Registration registration) { SessionInfoProto sessionInfo = this.getValidateSessionInfo(registration); if (sessionInfo != null) { - lwM2mTransportContextServer.sentParametersOnThingsboard(msg, topicName, sessionInfo); + lwM2mTransportContextServer.sendParametersOnThingsboard(msg, topicName, sessionInfo); } else { log.error("Client: [{}] updateParametersOnThingsboard [{}] sessionInfo ", registration, null); } @@ -458,7 +486,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { private void initLwM2mFromClientValue(Registration registration, LwM2mClient lwM2MClient) { LwM2mClientProfile lwM2MClientProfile = lwM2mClientContext.getProfile(registration); Set clientObjects = this.getAllOjectsInClient(registration); - if (clientObjects != null && !LwM2mTransportHandler.getClientOnlyObserveAfterConnect(lwM2MClientProfile)) { + if (clientObjects != null && LWM2M_STRATEGY_2 == LwM2mTransportHandler.getClientOnlyObserveAfterConnect(lwM2MClientProfile)) { // #2 lwM2MClient.getPendingRequests().addAll(clientObjects); clientObjects.forEach(path -> lwM2mTransportRequest.sendAllRequest(registration, path, GET_TYPE_OPER_READ, ContentFormat.TLV.getName(), @@ -499,20 +527,23 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { * #1 Return old Value Resource from LwM2MClient * #2 Update new Resources (replace old Resource Value on new Resource Value) * - * @param registration - Registration LwM2M Client + * @param registration - Registration LwM2M Client * @param lwM2mResource - LwM2mSingleResource response.getContent() - * @param path - resource + * @param path - resource */ private void updateResourcesValue(Registration registration, LwM2mResource lwM2mResource, String path) { LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClientWithReg(registration, null); - lwM2MClient.updateResourceValue(path, lwM2mResource); - Set paths = new HashSet<>(); - paths.add(path); - this.updateAttrTelemetry(registration, paths); + if (lwM2MClient.saveResourceValue(path, lwM2mResource, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getModelProvider())) { + Set paths = new HashSet<>(); + paths.add(path); + this.updateAttrTelemetry(registration, paths); + } else { + log.error("Fail update Resource [{}]", lwM2mResource); + } } /** - * Sent Attribute and Telemetry to Thingsboard + * send Attribute and Telemetry to Thingsboard * #1 - get AttrName/TelemetryName with value from LwM2MClient: * -- resourceId == path from LwM2MClientProfile.postAttributeProfile/postTelemetryProfile/postObserveProfile * -- AttrName/TelemetryName == resourceName from ModelObject.objectModel, value from ModelObject.instance.resource(resourceId) @@ -524,12 +555,9 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { JsonObject attributes = new JsonObject(); JsonObject telemetries = new JsonObject(); try { - writeLock.lock(); this.getParametersFromProfile(attributes, telemetries, registration, paths); } catch (Exception e) { log.error("UpdateAttrTelemetry", e); - } finally { - writeLock.unlock(); } if (attributes.getAsJsonObject().entrySet().size() > 0) this.updateParametersOnThingsboard(attributes, DEVICE_ATTRIBUTES_TOPIC, registration); @@ -539,13 +567,14 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { /** * @param clientProfile - - * @param path - + * @param path - * @return true if path isPresent in postAttributeProfile */ private boolean validatePathInAttrProfile(LwM2mClientProfile clientProfile, String path) { try { - List attributesSet = new Gson().fromJson(clientProfile.getPostAttributeProfile(), new TypeToken<>() { - }.getType()); + List attributesSet = new Gson().fromJson(clientProfile.getPostAttributeProfile(), + new TypeToken>() { + }.getType()); return attributesSet.stream().anyMatch(p -> p.equals(path)); } catch (Exception e) { log.error("Fail Validate Path [{}] ClientProfile.Attribute", path, e); @@ -555,12 +584,12 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { /** * @param clientProfile - - * @param path - + * @param path - * @return true if path isPresent in postAttributeProfile */ private boolean validatePathInTelemetryProfile(LwM2mClientProfile clientProfile, String path) { try { - List telemetriesSet = new Gson().fromJson(clientProfile.getPostTelemetryProfile(), new TypeToken<>() { + List telemetriesSet = new Gson().fromJson(clientProfile.getPostTelemetryProfile(), new TypeToken>() { }.getType()); return telemetriesSet.stream().anyMatch(p -> p.equals(path)); } catch (Exception e) { @@ -581,22 +610,25 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { Set clientInstances = this.getAllInstancesInClient(registration); Set result; if (GET_TYPE_OPER_READ.equals(typeOper)) { - result = JacksonUtil.fromString(lwM2MClientProfile.getPostAttributeProfile().toString(), new TypeReference<>() {}); - result.addAll(JacksonUtil.fromString(lwM2MClientProfile.getPostTelemetryProfile().toString(), new TypeReference<>() {})); + result = JacksonUtil.fromString(lwM2MClientProfile.getPostAttributeProfile().toString(), new TypeReference<>() { + }); + result.addAll(JacksonUtil.fromString(lwM2MClientProfile.getPostTelemetryProfile().toString(), new TypeReference<>() { + })); } else { - result = JacksonUtil.fromString(lwM2MClientProfile.getPostObserveProfile().toString(), new TypeReference<>() {}); + result = JacksonUtil.fromString(lwM2MClientProfile.getPostObserveProfile().toString(), new TypeReference<>() { + }); } - Set pathSent = ConcurrentHashMap.newKeySet(); + Set pathSend = ConcurrentHashMap.newKeySet(); result.forEach(target -> { // #1.1 String[] resPath = target.split("/"); String instance = "/" + resPath[1] + "/" + resPath[2]; if (clientInstances != null && clientInstances.size() > 0 && clientInstances.contains(instance)) { - pathSent.add(target); + pathSend.add(target); } }); - lwM2MClient.getPendingRequests().addAll(pathSent); - pathSent.forEach(target -> lwM2mTransportRequest.sendAllRequest(registration, target, typeOper, ContentFormat.TLV.getName(), + lwM2MClient.getPendingRequests().addAll(pathSend); + pathSend.forEach(target -> lwM2mTransportRequest.sendAllRequest(registration, target, typeOper, ContentFormat.TLV.getName(), null, null, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout())); if (GET_TYPE_OPER_OBSERVE.equals(typeOper)) { lwM2MClient.initValue(this, null); @@ -646,7 +678,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { Arrays.stream(registration.getObjectLinks()).forEach(url -> { LwM2mPath pathIds = new LwM2mPath(url.getUrl()); if (pathIds.isObjectInstance()) { - clientInstances.add(url.getUrl()); + clientInstances.add(convertToIdVerFromObjectId(url.getUrl(), registration)); } }); return (clientInstances.size() > 0) ? clientInstances : null; @@ -656,26 +688,22 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { * @param attributes - new JsonObject * @param telemetry - new JsonObject * @param registration - Registration LwM2M Client - * @param path - + * @param path - */ private void getParametersFromProfile(JsonObject attributes, JsonObject telemetry, Registration registration, Set path) { - LwM2mClientProfile lwM2MClientProfile = lwM2mClientContext.getProfile(registration); - lwM2MClientProfile.getPostAttributeProfile().forEach(p -> { - LwM2mPath pathIds = new LwM2mPath(p.getAsString()); - if (pathIds.isResource()) { - if (path == null || path.contains(p.getAsString())) { - this.addParameters(p.getAsString(), attributes, registration); + if (path != null && path.size() > 0) { + LwM2mClientProfile lwM2MClientProfile = lwM2mClientContext.getProfile(registration); + lwM2MClientProfile.getPostAttributeProfile().forEach(idVer -> { + if (path.contains(idVer.getAsString())) { + this.addParameters(idVer.getAsString(), attributes, registration); } - } - }); - lwM2MClientProfile.getPostTelemetryProfile().forEach(p -> { - LwM2mPath pathIds = new LwM2mPath(p.getAsString()); - if (pathIds.isResource()) { - if (path == null || path.contains(p.getAsString())) { - this.addParameters(p.getAsString(), telemetry, registration); + }); + lwM2MClientProfile.getPostTelemetryProfile().forEach(idVer -> { + if (path.contains(idVer.getAsString())) { + this.addParameters(idVer.getAsString(), telemetry, registration); } - } - }); + }); + } } /** @@ -685,7 +713,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { private void addParameters(String path, JsonObject parameters, Registration registration) { LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClientWithReg(registration, null); JsonObject names = lwM2mClientContext.getProfiles().get(lwM2MClient.getProfileId()).getPostKeyNameProfile(); - String resName = String.valueOf(names.get(path)); + String resName = names.get(path).getAsString(); if (resName != null && !resName.isEmpty()) { try { String resValue = this.getResourceValueToString(lwM2MClient, path); @@ -703,22 +731,21 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { * @return - value of Resource or null */ private String getResourceValueToString(LwM2mClient lwM2MClient, String path) { - LwM2mPath pathIds = new LwM2mPath(path); - ResourceValue resourceValue = this.returnResourceValueFromLwM2MClient(lwM2MClient, pathIds); + LwM2mPath pathIds = new LwM2mPath(convertToObjectIdFromIdVer(path)); + LwM2mResource resourceValue = this.returnResourceValueFromLwM2MClient(lwM2MClient, path); return resourceValue == null ? null : - this.converter.convertValue(resourceValue.getResourceValue(), this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getResourceModelType(lwM2MClient.getRegistration(), pathIds), ResourceModel.Type.STRING, pathIds).toString(); + this.converter.convertValue(resourceValue.isMultiInstances() ? resourceValue.getValues() : resourceValue.getValue(), resourceValue.getType(), ResourceModel.Type.STRING, pathIds).toString(); } /** - * * @param lwM2MClient - - * @param pathIds - + * @param path - * @return - return value of Resource by idPath */ - private ResourceValue returnResourceValueFromLwM2MClient(LwM2mClient lwM2MClient, LwM2mPath pathIds) { - ResourceValue resourceValue = null; - if (pathIds.isResource()) { - resourceValue = lwM2MClient.getResources().get(pathIds.toString()); + private LwM2mResource returnResourceValueFromLwM2MClient(LwM2mClient lwM2MClient, String path) { + LwM2mResource resourceValue = null; + if (new LwM2mPath(convertToObjectIdFromIdVer(path)).isResource()) { + resourceValue = lwM2MClient.getResources().get(path).getLwM2mResource(); } return resourceValue; } @@ -742,15 +769,15 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { * #3.2 Telemetry isChange (add&del) * #3.3 KeyName isChange (add) * #4 update - * #4.1 add If #3 isChange, then analyze and update Value in Transport form Client and sent Value to thingsboard + * #4.1 add If #3 isChange, then analyze and update Value in Transport form Client and send Value to thingsboard * #4.2 del * -- if add attributes includes del telemetry - result del for observe * #5 * #5.1 Observe isChange (add&del) * #5.2 Observe.add - * -- path Attr/Telemetry includes newObserve and does not include oldObserve: sent Request observe to Client + * -- path Attr/Telemetry includes newObserve and does not include oldObserve: send Request observe to Client * #5.3 Observe.del - * -- different between newObserve and oldObserve: sent Request cancel observe to client + * -- different between newObserve and oldObserve: send Request cancel observe to client * * @param registrationIds - * @param deviceProfile - @@ -775,20 +802,20 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { JsonObject keyNameNew = lwM2MClientProfileNew.getPostKeyNameProfile(); // #3 - ResultsAnalyzerParameters sentAttrToThingsboard = new ResultsAnalyzerParameters(); + ResultsAnalyzerParameters sendAttrToThingsboard = new ResultsAnalyzerParameters(); // #3.1 if (!attributeOld.equals(attributeNew)) { ResultsAnalyzerParameters postAttributeAnalyzer = this.getAnalyzerParameters(new Gson().fromJson(attributeOld, new TypeToken>() { }.getType()), attributeSetNew); - sentAttrToThingsboard.getPathPostParametersAdd().addAll(postAttributeAnalyzer.getPathPostParametersAdd()); - sentAttrToThingsboard.getPathPostParametersDel().addAll(postAttributeAnalyzer.getPathPostParametersDel()); + sendAttrToThingsboard.getPathPostParametersAdd().addAll(postAttributeAnalyzer.getPathPostParametersAdd()); + sendAttrToThingsboard.getPathPostParametersDel().addAll(postAttributeAnalyzer.getPathPostParametersDel()); } // #3.2 if (!telemetryOld.equals(telemetryNew)) { ResultsAnalyzerParameters postTelemetryAnalyzer = this.getAnalyzerParameters(new Gson().fromJson(telemetryOld, new TypeToken>() { }.getType()), telemetrySetNew); - sentAttrToThingsboard.getPathPostParametersAdd().addAll(postTelemetryAnalyzer.getPathPostParametersAdd()); - sentAttrToThingsboard.getPathPostParametersDel().addAll(postTelemetryAnalyzer.getPathPostParametersDel()); + sendAttrToThingsboard.getPathPostParametersAdd().addAll(postTelemetryAnalyzer.getPathPostParametersAdd()); + sendAttrToThingsboard.getPathPostParametersDel().addAll(postTelemetryAnalyzer.getPathPostParametersDel()); } // #3.3 if (!keyNameOld.equals(keyNameNew)) { @@ -796,52 +823,53 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { }.getType()), new Gson().fromJson(keyNameNew.toString(), new TypeToken>() { }.getType())); - sentAttrToThingsboard.getPathPostParametersAdd().addAll(keyNameChange.getPathPostParametersAdd()); + sendAttrToThingsboard.getPathPostParametersAdd().addAll(keyNameChange.getPathPostParametersAdd()); } // #4.1 add - if (sentAttrToThingsboard.getPathPostParametersAdd().size() > 0) { + if (sendAttrToThingsboard.getPathPostParametersAdd().size() > 0) { // update value in Resources registrationIds.forEach(registrationId -> { -// LeshanServer lwServer = leshanServer; Registration registration = lwM2mClientContext.getRegistration(registrationId); - this.readResourceValueObserve(registration, sentAttrToThingsboard.getPathPostParametersAdd(), GET_TYPE_OPER_READ); - // sent attr/telemetry to tingsboard for new path - this.updateAttrTelemetry(registration, sentAttrToThingsboard.getPathPostParametersAdd()); + this.readResourceValueObserve(registration, sendAttrToThingsboard.getPathPostParametersAdd(), GET_TYPE_OPER_READ); + // send attr/telemetry to tingsboard for new path + this.updateAttrTelemetry(registration, sendAttrToThingsboard.getPathPostParametersAdd()); }); } // #4.2 del - if (sentAttrToThingsboard.getPathPostParametersDel().size() > 0) { - ResultsAnalyzerParameters sentAttrToThingsboardDel = this.getAnalyzerParameters(sentAttrToThingsboard.getPathPostParametersAdd(), sentAttrToThingsboard.getPathPostParametersDel()); - sentAttrToThingsboard.setPathPostParametersDel(sentAttrToThingsboardDel.getPathPostParametersDel()); + if (sendAttrToThingsboard.getPathPostParametersDel().size() > 0) { + ResultsAnalyzerParameters sendAttrToThingsboardDel = this.getAnalyzerParameters(sendAttrToThingsboard.getPathPostParametersAdd(), sendAttrToThingsboard.getPathPostParametersDel()); + sendAttrToThingsboard.setPathPostParametersDel(sendAttrToThingsboardDel.getPathPostParametersDel()); } // #5.1 if (!observeOld.equals(observeNew)) { - Set observeSetOld = new Gson().fromJson(observeOld, new TypeToken<>() {}.getType()); - Set observeSetNew = new Gson().fromJson(observeNew, new TypeToken<>() {}.getType()); + Set observeSetOld = new Gson().fromJson(observeOld, new TypeToken>() { + }.getType()); + Set observeSetNew = new Gson().fromJson(observeNew, new TypeToken>() { + }.getType()); //#5.2 add // path Attr/Telemetry includes newObserve attributeSetOld.addAll(telemetrySetOld); - ResultsAnalyzerParameters sentObserveToClientOld = this.getAnalyzerParametersIn(attributeSetOld, observeSetOld); // add observe + ResultsAnalyzerParameters sendObserveToClientOld = this.getAnalyzerParametersIn(attributeSetOld, observeSetOld); // add observe attributeSetNew.addAll(telemetrySetNew); - ResultsAnalyzerParameters sentObserveToClientNew = this.getAnalyzerParametersIn(attributeSetNew, observeSetNew); // add observe + ResultsAnalyzerParameters sendObserveToClientNew = this.getAnalyzerParametersIn(attributeSetNew, observeSetNew); // add observe // does not include oldObserve - ResultsAnalyzerParameters postObserveAnalyzer = this.getAnalyzerParameters(sentObserveToClientOld.getPathPostParametersAdd(), sentObserveToClientNew.getPathPostParametersAdd()); - // sent Request observe to Client + ResultsAnalyzerParameters postObserveAnalyzer = this.getAnalyzerParameters(sendObserveToClientOld.getPathPostParametersAdd(), sendObserveToClientNew.getPathPostParametersAdd()); + // send Request observe to Client registrationIds.forEach(registrationId -> { Registration registration = lwM2mClientContext.getRegistration(registrationId); this.readResourceValueObserve(registration, postObserveAnalyzer.getPathPostParametersAdd(), GET_TYPE_OPER_OBSERVE); // 5.3 del - // sent Request cancel observe to Client + // send Request cancel observe to Client this.cancelObserveIsValue(registration, postObserveAnalyzer.getPathPostParametersDel()); }); } } } - private Set convertJsonArrayToSet (JsonArray jsonArray) { - List attributeListOld = new Gson().fromJson(jsonArray, new TypeToken<>() { + private Set convertJsonArrayToSet(JsonArray jsonArray) { + List attributeListOld = new Gson().fromJson(jsonArray, new TypeToken>() { }.getType()); return Sets.newConcurrentHashSet(attributeListOld); } @@ -874,14 +902,14 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { /** * Update Resource value after change RezAttrTelemetry in config Profile - * sent response Read to Client and add path to pathResAttrTelemetry in LwM2MClient.getAttrTelemetryObserveValue() + * send response Read to Client and add path to pathResAttrTelemetry in LwM2MClient.getAttrTelemetryObserveValue() * * @param registration - Registration LwM2M Client * @param targets - path Resources == [ "/2/0/0", "/2/0/1"] */ private void readResourceValueObserve(Registration registration, Set targets, String typeOper) { targets.forEach(target -> { - LwM2mPath pathIds = new LwM2mPath(target); + LwM2mPath pathIds = new LwM2mPath(convertToObjectIdFromIdVer(target)); if (pathIds.isResource()) { if (GET_TYPE_OPER_READ.equals(typeOper)) { lwM2mTransportRequest.sendAllRequest(registration, target, typeOper, @@ -907,8 +935,8 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { private void cancelObserveIsValue(Registration registration, Set paramAnallyzer) { LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClientWithReg(registration, null); paramAnallyzer.forEach(p -> { - if (this.returnResourceValueFromLwM2MClient(lwM2MClient, new LwM2mPath(p)) != null) { - this.setCancelObservationRecourse(registration, p); + if (this.returnResourceValueFromLwM2MClient(lwM2MClient, p) != null) { + this.setCancelObservationRecourse(registration, convertToObjectIdFromIdVer(p)); } } ); @@ -953,8 +981,9 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { */ private String getPathAttributeUpdateProfile(TransportProtos.SessionInfoProto sessionInfo, String name) { LwM2mClientProfile profile = lwM2mClientContext.getProfile(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB())); + LwM2mClient lwM2mClient = lwM2mClientContext.getLwM2MClient(sessionInfo); return profile.getPostKeyNameProfile().getAsJsonObject().entrySet().stream() - .filter(e -> e.getValue().getAsString().equals(name)).findFirst().map(Map.Entry::getKey) + .filter(e -> e.getValue().getAsString().equals(name) && validateResourceInModel(lwM2mClient, e.getKey(), false)).findFirst().map(Map.Entry::getKey) .orElse(""); } @@ -963,7 +992,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { * #1 Get path resource by result attributesResponse * #1.1 If two names have equal path => last time attribute * #2.1 if there is a difference in values between the current resource values and the shared attribute values - * => sent to client Request Update of value (new value from shared attribute) + * => send to client Request Update of value (new value from shared attribute) * and LwM2MClient.delayedRequests.add(path) * #2.1 if there is not a difference in values between the current resource values and the shared attribute values * @@ -975,11 +1004,13 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2MClient(sessionInfo); attributesResponse.getSharedAttributeListList().forEach(attr -> { String path = this.getPathAttributeUpdate(sessionInfo, attr.getKv().getKey()); - // #1.1 - if (lwM2MClient.getDelayedRequests().containsKey(path) && attr.getTs() > lwM2MClient.getDelayedRequests().get(path).getTs()) { - lwM2MClient.getDelayedRequests().put(path, attr); - } else { - lwM2MClient.getDelayedRequests().put(path, attr); + if (path != null) { + // #1.1 + if (lwM2MClient.getDelayedRequests().containsKey(path) && attr.getTs() > lwM2MClient.getDelayedRequests().get(path).getTs()) { + lwM2MClient.getDelayedRequests().put(path, attr); + } else { + lwM2MClient.getDelayedRequests().put(path, attr); + } } }); // #2.1 @@ -1057,6 +1088,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { } /** + * !!! sharedAttr === profileAttr !!! * If there is a difference in values between the current resource values and the shared attribute values * when the client connects to the server * #1 get attributes name from profile include name resources in ModelObject if resource isWritable @@ -1083,24 +1115,37 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService { /** - * Get names and keyNames from profile shared!!!! attr resources IsWritable + * !!! sharedAttr === profileAttr !!! + * Get names or keyNames from profile: resources IsWritable * * @param lwM2MClient - - * @return ArrayList keyNames from profile attr resources shared!!!! && IsWritable + * @return ArrayList keyNames from profile profileAttr && IsWritable */ private List getNamesAttrFromProfileIsWritable(LwM2mClient lwM2MClient) { LwM2mClientProfile profile = lwM2mClientContext.getProfile(lwM2MClient.getProfileId()); - Set attrSet = new Gson().fromJson(profile.getPostAttributeProfile(), new TypeToken<>() {}.getType()); - ConcurrentMap keyNamesMap = new Gson().fromJson(profile.getPostKeyNameProfile().toString(), new TypeToken>() {}.getType()); + Set attrSet = new Gson().fromJson(profile.getPostAttributeProfile(), + new TypeToken>() { + }.getType()); + ConcurrentMap keyNamesMap = new Gson().fromJson(profile.getPostKeyNameProfile().toString(), + new TypeToken>() { + }.getType()); ConcurrentMap keyNamesIsWritable = keyNamesMap.entrySet() .stream() - .filter(e -> (attrSet.contains(e.getKey()) && lwM2mTransportContextServer.getLwM2MTransportConfigServer().getResourceModel(lwM2MClient.getRegistration(), new LwM2mPath(e.getKey())) != null && - lwM2mTransportContextServer.getLwM2MTransportConfigServer().getResourceModel(lwM2MClient.getRegistration(), new LwM2mPath(e.getKey())).operations.isWritable())) + .filter(e -> (attrSet.contains(e.getKey()) && validateResourceInModel(lwM2MClient, e.getKey(), true))) .collect(Collectors.toConcurrentMap(Map.Entry::getKey, Map.Entry::getValue)); Set namesIsWritable = ConcurrentHashMap.newKeySet(); namesIsWritable.addAll(new HashSet<>(keyNamesIsWritable.values())); return new ArrayList<>(namesIsWritable); } + + private boolean validateResourceInModel(LwM2mClient lwM2mClient, String pathKey, boolean isWritable) { + ResourceModel resourceModel = lwM2mClient.getResourceModel(pathKey); + Integer objectId = validateObjectIdFromKey(pathKey); + String objectVer = validateObjectVerFromKey(pathKey); + return resourceModel != null && (isWritable ? + objectId != null && objectVer != null && objectVer.equals(lwM2mClient.getRegistration().getSupportedVersion(objectId)) && resourceModel.operations.isWritable() : + objectId != null && objectVer != null && objectVer.equals(lwM2mClient.getRegistration().getSupportedVersion(objectId))); + } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java index 503af176af..3705547325 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java @@ -22,6 +22,7 @@ import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.server.model.LwM2mModelProvider; import org.eclipse.leshan.server.registration.Registration; +import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; @@ -30,8 +31,10 @@ import java.util.Base64; import java.util.Collection; import java.util.Iterator; import java.util.Map; +import java.util.Optional; import static org.thingsboard.server.common.data.ResourceType.LWM2M_MODEL; +import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_KEY; @Slf4j public class LwM2mVersionedModelProvider implements LwM2mModelProvider { @@ -49,12 +52,9 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider { this.lwM2mClientContext = lwM2mClientContext; this.lwM2mTransportContextServer = lwM2mTransportContextServer; } - private String getIdVer(ObjectModel objectModel) { - return objectModel.id + "##" + ((objectModel.getVersion() == null || objectModel.getVersion().isEmpty()) ? ObjectModel.DEFAULT_VERSION : objectModel.getVersion()); - } - private String getIdVer(Integer objectId, String version) { - return objectId != null ? objectId + "##" + ((version == null || version.isEmpty()) ? ObjectModel.DEFAULT_VERSION : version) : null; + private String getKeyIdVer(Integer objectId, String version) { + return objectId != null ? objectId + LWM2M_SEPARATOR_KEY + ((version == null || version.isEmpty()) ? ObjectModel.DEFAULT_VERSION : version) : null; } /** @@ -65,8 +65,7 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider { */ @Override public LwM2mModel getObjectModel(Registration registration) { - return new DynamicModel(registration - ); + return new DynamicModel(registration); } private class DynamicModel implements LwM2mModel { @@ -86,6 +85,7 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider { if (objectModel != null) return objectModel.resources.get(resourceId); else + log.warn("TbResources (Object model) with id [{}/0/{}] not found on the server", objectId, resourceId); return null; } catch (Exception e) { log.error("", e); @@ -105,12 +105,11 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider { @Override public Collection getObjectModels() { Map supportedObjects = this.registration.getSupportedObject(); - Collection result = new ArrayList(supportedObjects.size()); - Iterator i$ = supportedObjects.entrySet().iterator(); - + Collection result = new ArrayList<>(supportedObjects.size()); + Iterator> i$ = supportedObjects.entrySet().iterator(); while (i$.hasNext()) { - Map.Entry supportedObject = (Map.Entry) i$.next(); - ObjectModel objectModel = this.getObjectModelDynamic((Integer) supportedObject.getKey(), (String) supportedObject.getValue()); + Map.Entry supportedObject = i$.next(); + ObjectModel objectModel = this.getObjectModelDynamic(supportedObject.getKey(), supportedObject.getValue()); if (objectModel != null) { result.add(objectModel); } @@ -119,18 +118,16 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider { } private ObjectModel getObjectModelDynamic(Integer objectId, String version) { - String key = getIdVer(objectId, version); - String xmlB64 = lwM2mTransportContextServer.getTransportResourceCache().get( - this.tenantId, - LWM2M_MODEL, - key). - getValue(); - return xmlB64 != null && !xmlB64.isEmpty() ? - lwM2mTransportContextServer.parseFromXmlToObjectModel( - Base64.getDecoder().decode(xmlB64), - key + ".xml", - new DefaultDDFFileValidator()) : - null; + String key = getKeyIdVer(objectId, version); + + Optional tbResource = lwM2mTransportContextServer + .getTransportResourceCache() + .get(this.tenantId, LWM2M_MODEL, key); + + return tbResource.map(resource -> lwM2mTransportContextServer.parseFromXmlToObjectModel( + Base64.getDecoder().decode(resource.getData()), + key + ".xml", + new DefaultDDFFileValidator())).orElse(null); } } } 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 1c9afaab6d..bdb783e3ff 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 @@ -17,9 +17,10 @@ package org.thingsboard.server.transport.lwm2m.server.client; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import org.eclipse.leshan.core.node.LwM2mMultipleResource; +import org.eclipse.leshan.core.model.ResourceModel; +import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mResource; -import org.eclipse.leshan.core.node.LwM2mSingleResource; +import org.eclipse.leshan.server.model.LwM2mModelProvider; import org.eclipse.leshan.server.registration.Registration; import org.eclipse.leshan.server.security.SecurityInfo; import org.thingsboard.server.gen.transport.TransportProtos; @@ -28,9 +29,14 @@ import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServiceImpl; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; + +import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.convertToObjectIdFromIdVer; @Slf4j @Data @@ -67,21 +73,79 @@ public class LwM2mClient implements Cloneable { this.init = false; } - public void updateResourceValue(String pathRez, LwM2mResource rez) { - if (rez instanceof LwM2mMultipleResource) { - this.resources.put(pathRez, new ResourceValue(rez.getValues(), null, true)); - } else if (rez instanceof LwM2mSingleResource) { - this.resources.put(pathRez, new ResourceValue(null, rez.getValue(), false)); + public boolean saveResourceValue(String pathRez, LwM2mResource rez, LwM2mModelProvider modelProvider) { + if (this.resources.get(pathRez) != null && this.resources.get(pathRez).getResourceModel() != null) { + this.resources.get(pathRez).setLwM2mResource(rez); + return true; + } else { + LwM2mPath pathIds = new LwM2mPath(convertToObjectIdFromIdVer(pathRez)); + ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()); + if (resourceModel != null) { + this.resources.put(pathRez, new ResourceValue(rez, resourceModel)); + return true; + } else { + return false; + } + } + } + + public ResourceModel getResourceModel(String pathRez) { + if (this.getResources().get(pathRez) != null) { + return this.getResources().get(pathRez).getResourceModel(); + } else { + return null; } } - public void initValue(LwM2mTransportServiceImpl lwM2MTransportService, String path) { + /** + * + * @param pathIdVer == "3_1.0" + * @param modelProvider - + */ + public void deleteResources(String pathIdVer, LwM2mModelProvider modelProvider) { + Set key = getKeysEqualsIdVer(pathIdVer); + key.forEach(pathRez -> { + LwM2mPath pathIds = new LwM2mPath(convertToObjectIdFromIdVer(pathRez.toString())); + ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()); + if (resourceModel != null) { + this.resources.get(pathRez).setResourceModel(resourceModel); + } + else { + this.resources.remove(pathRez); + } + }); + } + + /** + * + * @param idVer - + * @param modelProvider - + */ + public void updateResourceModel(String idVer, LwM2mModelProvider modelProvider) { + Set key = getKeysEqualsIdVer(idVer); + key.forEach(k -> this.saveResourceModel(k.toString(), modelProvider)); + } + + private void saveResourceModel(String pathRez, LwM2mModelProvider modelProvider) { + LwM2mPath pathIds = new LwM2mPath(convertToObjectIdFromIdVer(pathRez)); + ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()); + this.resources.get(pathRez).setResourceModel(resourceModel); + } + + private Set getKeysEqualsIdVer(String idVer) { + return this.resources.keySet() + .stream() + .filter(e -> idVer.equals(e.split(LWM2M_SEPARATOR_PATH)[1])) + .collect(Collectors.toSet()); + } + + public void initValue(LwM2mTransportServiceImpl serviceImpl, String path) { if (path != null) { this.pendingRequests.remove(path); } if (this.pendingRequests.size() == 0) { this.init = true; - lwM2MTransportService.putDelayedUpdateResourcesThingsboard(this); + serviceImpl.putDelayedUpdateResourcesThingsboard(this); } } 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 d30628a43f..926d12516b 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 @@ -36,6 +36,7 @@ import static org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode.NO @Service @TbLwM2mTransportComponent public class LwM2mClientContextImpl implements LwM2mClientContext { + private static final boolean INFOS_ARE_COMPROMISED = false; private final Map lwM2mClients = new ConcurrentHashMap<>(); @@ -51,10 +52,10 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { } public void delRemoveSessionAndListener(String registrationId) { - LwM2mClient lwM2MClient = lwM2mClients.get(registrationId); + LwM2mClient lwM2MClient = this.lwM2mClients.get(registrationId); if (lwM2MClient != null) { - securityStore.remove(lwM2MClient.getEndpoint(), INFOS_ARE_COMPROMISED); - lwM2mClients.remove(registrationId); + this.securityStore.remove(lwM2MClient.getEndpoint(), INFOS_ARE_COMPROMISED); + this.lwM2mClients.remove(registrationId); } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientProfile.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientProfile.java index 8285c9bc8b..1c4042bd1a 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientProfile.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientProfile.java @@ -34,25 +34,25 @@ public class LwM2mClientProfile { /** * {"keyName": { - * "/3/0/1": "modelNumber", - * "/3/0/0": "manufacturer", - * "/3/0/2": "serialNumber" + * "/3_1.0/0/1": "modelNumber", + * "/3_1.0/0/0": "manufacturer", + * "/3_1.0/0/2": "serialNumber" * } **/ private JsonObject postKeyNameProfile; /** - * [ "/2/0/0", "/2/0/1"] + * [ "/3_1.0/0/0", "/3_1.0/0/1"] */ private JsonArray postAttributeProfile; /** - * [ "/2/0/0", "/2/0/1"] + * [ "/3_1.0/0/0", "/3_1.0/0/2"] */ private JsonArray postTelemetryProfile; /** - * [ "/2/0/0", "/2/0/1"] + * [ "/3_1.0/0", "/3_1.0/0/1, "/3_1.0/0/2"] */ private JsonArray postObserveProfile; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResourceValue.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResourceValue.java index 3ff04f288b..cbaf60ca77 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResourceValue.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResourceValue.java @@ -16,23 +16,16 @@ package org.thingsboard.server.transport.lwm2m.server.client; import lombok.Data; - -import java.util.Map; +import org.eclipse.leshan.core.model.ResourceModel; +import org.eclipse.leshan.core.node.LwM2mResource; @Data public class ResourceValue { - Map values; - Object value; - boolean multiInstances; - - public ResourceValue(Map values, Object value, boolean multiInstances) { - this.values = values; - this.value = value; - this.multiInstances = multiInstances; - } + private LwM2mResource lwM2mResource; + private ResourceModel resourceModel; - public Object getResourceValue() { - return this.multiInstances ? this.values : this.value; + public ResourceValue(LwM2mResource lwM2mResource, ResourceModel resourceModel) { + this.lwM2mResource = lwM2mResource; + this.resourceModel = resourceModel; } - } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/LwM2mInMemorySecurityStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/LwM2mInMemorySecurityStore.java deleted file mode 100644 index fc66a5fed0..0000000000 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/LwM2mInMemorySecurityStore.java +++ /dev/null @@ -1,251 +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.server.store; - -import lombok.extern.slf4j.Slf4j; -import org.eclipse.leshan.core.util.Hex; -import org.eclipse.leshan.server.registration.Registration; -import org.eclipse.leshan.server.security.InMemorySecurityStore; -import org.eclipse.leshan.server.security.SecurityInfo; -import org.eclipse.leshan.server.security.SecurityStoreListener; -import org.springframework.beans.factory.annotation.Autowired; -import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode; -import org.thingsboard.server.transport.lwm2m.secure.LwM2mCredentialsSecurityInfoValidator; -import org.thingsboard.server.transport.lwm2m.secure.ReadResultSecurityStore; -import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler; -import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; -import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientProfile; -import org.thingsboard.server.transport.lwm2m.utils.TypeServer; - -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; -import java.util.stream.Collectors; - -import static org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode.NO_SEC; - -@Slf4j -//@Service("LwM2mInMemorySecurityStore") -//@TbLwM2mTransportComponent -@Deprecated -public class LwM2mInMemorySecurityStore extends InMemorySecurityStore { - private static final boolean INFOS_ARE_COMPROMISED = false; - - // lock for the two maps - private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); - private final Lock readLock = readWriteLock.readLock(); - private final Lock writeLock = readWriteLock.writeLock(); - private final Map sessions = new ConcurrentHashMap<>(); - private Map profiles = new ConcurrentHashMap<>(); - private SecurityStoreListener listener; - - @Autowired - LwM2mCredentialsSecurityInfoValidator lwM2MCredentialsSecurityInfoValidator; - - /** - * Start after DefaultAuthorizer or LwM2mPskStore - * @param endPoint - - * @return SecurityInfo - */ - @Override - public SecurityInfo getByEndpoint(String endPoint) { - readLock.lock(); - try { - String registrationId = this.getRegistrationId(endPoint, null); - return (registrationId != null && sessions.size() > 0 && sessions.get(registrationId) != null) ? - sessions.get(registrationId).getSecurityInfo() : this.addLwM2MClientToSession(endPoint); - } finally { - readLock.unlock(); - } - } - - /** - * Start after LwM2mPskStore - * @param identity - - * @return SecurityInfo - */ - @Override - public SecurityInfo getByIdentity(String identity) { - readLock.lock(); - try { - String integrationId = this.getRegistrationId(null, identity); - return (integrationId != null) ? sessions.get(integrationId).getSecurityInfo() : this.addLwM2MClientToSession(identity); - } finally { - readLock.unlock(); - } - } - - @Override - public Collection getAll() { - readLock.lock(); - try { - return this.sessions.values().stream().map(LwM2mClient::getSecurityInfo).collect(Collectors.toUnmodifiableList()); - } finally { - readLock.unlock(); - } - } - - /** - * Removed registration Client from sessions and listener - * @param registrationId if Client - */ - public void delRemoveSessionAndListener(String registrationId) { - writeLock.lock(); - try { - LwM2mClient lwM2MClient = (sessions.get(registrationId) != null) ? sessions.get(registrationId) : null; - if (lwM2MClient != null) { - if (listener != null) { - listener.securityInfoRemoved(INFOS_ARE_COMPROMISED, lwM2MClient.getSecurityInfo()); - } - sessions.remove(registrationId); - } - } finally { - writeLock.unlock(); - } - } - - @Override - public void setListener(SecurityStoreListener listener) { - this.listener = listener; - } - - public LwM2mClient getLwM2MClient(String endPoint, String identity) { - Map.Entry modelClients = endPoint != null ? - this.sessions.entrySet().stream().filter(model -> endPoint.equals(model.getValue().getEndpoint())).findAny().orElse(null) : - this.sessions.entrySet().stream().filter(model -> identity.equals(model.getValue().getIdentity())).findAny().orElse(null); - return modelClients != null ? modelClients.getValue() : null; - } - - public LwM2mClient getLwM2MClientWithReg(Registration registration, String registrationId) { - return registrationId != null ? - this.sessions.get(registrationId) : - this.sessions.containsKey(registration.getId()) ? - this.sessions.get(registration.getId()) : - this.sessions.get(registration.getEndpoint()); - } - - public LwM2mClient getLwM2MClient(TransportProtos.SessionInfoProto sessionInfo) { - return this.getSession(new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB())).entrySet().iterator().next().getValue(); - } - - /** - * Update in sessions (LwM2MClient for key registration_Id) after starting registration LwM2MClient in LwM2MTransportServiceImpl - * Remove from sessions LwM2MClient with key registration_Endpoint - * @param registration - - * @return LwM2MClient after adding it to session - */ - public LwM2mClient updateInSessionsLwM2MClient(Registration registration) { - writeLock.lock(); - try { - if (this.sessions.get(registration.getEndpoint()) == null) { - this.addLwM2MClientToSession(registration.getEndpoint()); - } - LwM2mClient lwM2MClient = this.sessions.get(registration.getEndpoint()); - lwM2MClient.setRegistration(registration); -// lwM2MClient.getAttributes().putAll(registration.getAdditionalRegistrationAttributes()); - this.sessions.remove(registration.getEndpoint()); - this.sessions.put(registration.getId(), lwM2MClient); - return lwM2MClient; - } finally { - writeLock.unlock(); - } - } - - private String getRegistrationId(String endPoint, String identity) { - List registrationIds = (endPoint != null) ? - this.sessions.entrySet().stream().filter(model -> endPoint.equals(model.getValue().getEndpoint())).map(Map.Entry::getKey).collect(Collectors.toList()) : - this.sessions.entrySet().stream().filter(model -> identity.equals(model.getValue().getIdentity())).map(Map.Entry::getKey).collect(Collectors.toList()); - return (registrationIds != null && registrationIds.size() > 0) ? registrationIds.get(0) : null; - } - - public Registration getByRegistration(String registrationId) { - return this.sessions.get(registrationId).getRegistration(); - } - - /** - * Add new LwM2MClient to session - * @param identity- - * @return SecurityInfo. If error - SecurityInfoError - * and log: - * - FORBIDDEN - if there is no authorization - * - profileUuid - if the device does not have a profile - * - device - if the thingsboard does not have a device with a name equal to the identity - */ - private SecurityInfo addLwM2MClientToSession(String identity) { - ReadResultSecurityStore store = lwM2MCredentialsSecurityInfoValidator.createAndValidateCredentialsSecurityInfo(identity, TypeServer.CLIENT); - if (store.getSecurityMode() < LwM2MSecurityMode.DEFAULT_MODE.code) { - UUID profileUuid = (store.getDeviceProfile() != null && addUpdateProfileParameters(store.getDeviceProfile())) ? store.getDeviceProfile().getUuidId() : null; - if (store.getSecurityInfo() != null && profileUuid != null) { - String endpoint = store.getSecurityInfo().getEndpoint(); - sessions.put(endpoint, new LwM2mClient(endpoint, store.getSecurityInfo().getIdentity(), store.getSecurityInfo(), store.getMsg(), profileUuid, UUID.randomUUID())); - } else if (store.getSecurityMode() == NO_SEC.code && profileUuid != null) { - sessions.put(identity, new LwM2mClient(identity, null, null, store.getMsg(), profileUuid, UUID.randomUUID())); - } else { - log.error("Registration failed: FORBIDDEN/profileUuid/device [{}] , endpointId: [{}]", profileUuid, identity); - /** - * Return Error securityInfo - */ - byte[] preSharedKey = Hex.decodeHex("0A0B".toCharArray()); - SecurityInfo infoError = SecurityInfo.newPreSharedKeyInfo("error", "error_identity", preSharedKey); - return infoError; - } - } - return store.getSecurityInfo(); - } - - public Map getSession(UUID sessionUuId) { - return this.sessions.entrySet().stream() - .filter(e -> e.getValue().getSessionId().equals(sessionUuId)) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - } - - public Map getSessions() { - return this.sessions; - } - - public Map getProfiles() { - return this.profiles; - } - - public LwM2mClientProfile getProfile(UUID profileUuId) { - return this.profiles.get(profileUuId); - } - - public LwM2mClientProfile getProfile(String registrationId) { - UUID profileUUid = this.getSessions().get(registrationId).getProfileId(); - return this.getProfiles().get(profileUUid); - } - - public Map setProfiles(Map profiles) { - return this.profiles = profiles; - } - - public boolean addUpdateProfileParameters(DeviceProfile deviceProfile) { - LwM2mClientProfile lwM2MClientProfile = LwM2mTransportHandler.getLwM2MClientProfileFromThingsboard(deviceProfile); - if (lwM2MClientProfile != null) { - profiles.put(deviceProfile.getUuidId(), lwM2MClientProfile); - return true; - } - return false; - } -} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreConfiguration.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreConfiguration.java index 9e4b7e442a..679e95bdbb 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreConfiguration.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreConfiguration.java @@ -29,6 +29,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.TBRedisCacheConfiguration; 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; @@ -89,10 +90,14 @@ public class TbLwM2mStoreConfiguration { } @Override - public SecurityInfo getByEndpoint(String endpoint) { - SecurityInfo securityInfo = securityStore.getByEndpoint(endpoint); + public SecurityInfo getByEndpoint(String endPoint) { + SecurityInfo securityInfo = securityStore.getByEndpoint(endPoint); if (securityInfo == null) { - securityInfo = clientContext.addLwM2mClientToSession(endpoint).getSecurityInfo(); + LwM2mClient lwM2mClient = clientContext.getLwM2MClient(endPoint, null); + if (lwM2mClient != null && lwM2mClient.getRegistration() != null && !lwM2mClient.getRegistration().getIdentity().isSecure()){ + return null; + } + securityInfo = clientContext.addLwM2mClientToSession(endPoint).getSecurityInfo(); try { if (securityInfo != null) { add(securityInfo); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java index 85283749dc..0faa20952e 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java @@ -145,6 +145,8 @@ public class LwM2mValueConverterImpl implements LwM2mValueConverter { } DateFormat formatter = new SimpleDateFormat(DATE_FORMAT); return formatter.format(new Date(timeValue)); + case OPAQUE: + return Hex.encodeHexString((byte[])value); default: break; } @@ -155,7 +157,7 @@ public class LwM2mValueConverterImpl implements LwM2mValueConverter { log.debug("Trying to convert hexadecimal string [{}] to byte array", value); // TODO check if we shouldn't instead assume that the string contains Base64 encoded data try { - return Hex.decodeHex(((String) value).toCharArray()); + return Hex.decodeHex(((String)value).toCharArray()); } catch (IllegalArgumentException e) { throw new CodecException("Unable to convert hexastring [%s] to byte array for resource %s", value, resourcePath); diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java index c2cf3686e9..64efb5f036 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java @@ -30,7 +30,7 @@ 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.gen.transport.TransportProtos; -import org.thingsboard.server.transport.mqtt.util.SslUtil; +import org.thingsboard.server.common.transport.util.SslUtil; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; @@ -41,7 +41,6 @@ import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import java.io.File; import java.io.FileInputStream; -import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.security.KeyStore; @@ -56,7 +55,7 @@ import java.util.concurrent.TimeUnit; */ @Slf4j @Component("MqttSslHandlerProvider") -@ConditionalOnExpression("'${transport.type:null}'=='null' || ('${transport.type}'=='local' && '${transport.mqtt.enabled}'=='true')") +@ConditionalOnExpression("'${transport.mqtt.enabled}'=='true'") @ConditionalOnProperty(prefix = "transport.mqtt.ssl", value = "enabled", havingValue = "true", matchIfMissing = false) public class MqttSslHandlerProvider { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index 06a8dcacdc..6056aa293d 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -66,7 +66,7 @@ import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; import org.thingsboard.server.transport.mqtt.session.DeviceSessionCtx; import org.thingsboard.server.transport.mqtt.session.GatewaySessionHandler; import org.thingsboard.server.transport.mqtt.session.MqttTopicMatcher; -import org.thingsboard.server.transport.mqtt.util.SslUtil; +import org.thingsboard.server.common.transport.util.SslUtil; import javax.net.ssl.SSLPeerUnverifiedException; import java.security.cert.Certificate; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java index a004fbd500..c948e755c2 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java @@ -146,7 +146,7 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { @Override public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { - return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC + rpcRequest.getRequestId(), rpcRequest.toByteArray())); + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC + rpcRequest.getRequestId(), ProtoConverter.convertToRpcRequest(rpcRequest))); } @Override diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java index 8209fe2531..51a953e4b2 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java @@ -18,11 +18,11 @@ package org.thingsboard.server.common.transport; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.SessionCloseNotificationProto; import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportUpdateCredentialsProto; import java.util.Optional; @@ -44,9 +44,12 @@ public interface SessionMsgListener { default void onToTransportUpdateCredentials(ToTransportUpdateCredentialsProto toTransportUpdateCredentials){} - default void onDeviceProfileUpdate(TransportProtos.SessionInfoProto newSessionInfo, DeviceProfile deviceProfile) { - } + default void onDeviceProfileUpdate(TransportProtos.SessionInfoProto newSessionInfo, DeviceProfile deviceProfile) {} + + default void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, + Optional deviceProfileOpt) {} + + default void onResourceUpdate(Optional resourceUpdateMsgOpt) {} - default void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, Optional deviceProfileOpt) { - } + default void onResourceDelete(Optional resourceUpdateMsgOpt) {} } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportResourceCache.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportResourceCache.java index 34e95189fa..7c72318de7 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportResourceCache.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportResourceCache.java @@ -15,13 +15,15 @@ */ package org.thingsboard.server.common.transport; -import org.thingsboard.server.common.data.Resource; import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.id.TenantId; +import java.util.Optional; + public interface TransportResourceCache { - Resource get(TenantId tenantId, ResourceType resourceType, String resourceId); + Optional get(TenantId tenantId, ResourceType resourceType, String resourceId); void update(TenantId tenantId, ResourceType resourceType, String resourceI); 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 d8c69bc7fa..8c6d2af02a 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 @@ -22,7 +22,6 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import com.google.gson.JsonSyntaxException; -import com.google.gson.stream.MalformedJsonException; import org.apache.commons.lang3.math.NumberUtils; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.DataConstants; @@ -161,15 +160,7 @@ public class JsonConverter { result.addProperty("id", msg.getRequestId()); } result.addProperty("method", msg.getMethodName()); - try { - result.add("params", JSON_PARSER.parse(msg.getParams())); - } catch (JsonSyntaxException ex) { - if (ex.getCause() instanceof MalformedJsonException) { - result.addProperty("params", msg.getParams()); - } else { - throw ex; - } - } + result.add("params", JSON_PARSER.parse(msg.getParams())); return result; } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java index fc82f78633..ed440ebcfe 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java @@ -15,7 +15,9 @@ */ package org.thingsboard.server.common.transport.adaptor; +import com.google.gson.JsonElement; import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; import com.google.protobuf.InvalidProtocolBufferException; import lombok.extern.slf4j.Slf4j; import org.springframework.util.CollectionUtils; @@ -167,4 +169,27 @@ public class ProtoConverter { }); return kvList; } + + public static byte[] convertToRpcRequest(TransportProtos.ToDeviceRpcRequestMsg toDeviceRpcRequestMsg) { + TransportProtos.ToDeviceRpcRequestMsg.Builder toDeviceRpcRequestMsgBuilder = toDeviceRpcRequestMsg.newBuilderForType(); + toDeviceRpcRequestMsgBuilder.mergeFrom(toDeviceRpcRequestMsg); + toDeviceRpcRequestMsgBuilder.setParams(parseParams(toDeviceRpcRequestMsg)); + TransportProtos.ToDeviceRpcRequestMsg result = toDeviceRpcRequestMsgBuilder.build(); + return result.toByteArray(); + } + + private static String parseParams(TransportProtos.ToDeviceRpcRequestMsg toDeviceRpcRequestMsg) { + String params = toDeviceRpcRequestMsg.getParams(); + JsonElement jsonElementParams = JSON_PARSER.parse(params); + if (!jsonElementParams.isJsonPrimitive()) { + return params; + } else { + JsonPrimitive primitiveParams = jsonElementParams.getAsJsonPrimitive(); + if (jsonElementParams.getAsJsonPrimitive().isString()) { + return primitiveParams.getAsString(); + } else { + return params; + } + } + } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java index ab18b930f9..b175ca8580 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java @@ -25,7 +25,15 @@ import java.util.UUID; public class SessionInfoCreator { public static TransportProtos.SessionInfoProto create(ValidateDeviceCredentialsResponse msg, TransportContext context, UUID sessionId) { - return TransportProtos.SessionInfoProto.newBuilder().setNodeId(context.getNodeId()) + return getSessionInfoProto(msg, context.getNodeId(), sessionId); + } + + public static TransportProtos.SessionInfoProto create(ValidateDeviceCredentialsResponse msg, String nodeId, UUID sessionId) { + return getSessionInfoProto(msg, nodeId, sessionId); + } + + private static TransportProtos.SessionInfoProto getSessionInfoProto(ValidateDeviceCredentialsResponse msg, String nodeId, UUID sessionId) { + return TransportProtos.SessionInfoProto.newBuilder().setNodeId(nodeId) .setSessionIdMSB(sessionId.getMostSignificantBits()) .setSessionIdLSB(sessionId.getLeastSignificantBits()) .setDeviceIdMSB(msg.getDeviceInfo().getDeviceId().getId().getMostSignificantBits()) diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/lwm2m/LwM2MTransportConfigServer.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/lwm2m/LwM2MTransportConfigServer.java index 1208ace313..5603721474 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/lwm2m/LwM2MTransportConfigServer.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/lwm2m/LwM2MTransportConfigServer.java @@ -18,11 +18,7 @@ package org.thingsboard.server.common.transport.lwm2m; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.eclipse.leshan.core.model.ObjectModel; -import org.eclipse.leshan.core.model.ResourceModel; -import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.server.model.LwM2mModelProvider; -import org.eclipse.leshan.server.registration.Registration; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; @@ -38,16 +34,12 @@ import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; -import java.util.List; @Slf4j @Component @ConditionalOnExpression("('${service.type:null}'=='tb-transport' && '${transport.lwm2m.enabled:false}'=='true') || '${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core'") public class LwM2MTransportConfigServer { - @Getter - private String MODEL_PATH_DEFAULT = "models"; - @Getter private String KEY_STORE_DEFAULT_RESOURCE_PATH = "credentials"; @@ -79,10 +71,6 @@ public class LwM2MTransportConfigServer { // private String PATH_DATA_MICROSERVICE = "/usr/share/tb-lwm2m-transport/data$"; private String PATH_DATA = "data"; - @Getter - @Setter - private List modelsValueCommon; - @Getter @Setter private LwM2mModelProvider modelProvider; @@ -95,10 +83,6 @@ public class LwM2MTransportConfigServer { @Value("${transport.sessions.report_timeout}") private long sessionReportTimeout; - @Getter - @Value("${transport.lwm2m.model_path_file:}") - private String modelPathFile; - @Getter @Value("${transport.lwm2m.recommended_ciphers:}") private boolean recommendedCiphers; @@ -108,12 +92,8 @@ public class LwM2MTransportConfigServer { private boolean recommendedSupportedGroups; @Getter - @Value("${transport.lwm2m.request_pool_size:}") - private int requestPoolSize; - - @Getter - @Value("${transport.lwm2m.request_error_pool_size:}") - private int requestErrorPoolSize; + @Value("${transport.lwm2m.response_pool_size:}") + private int responsePoolSize; @Getter @Value("${transport.lwm2m.registered_pool_size:}") @@ -232,19 +212,4 @@ public class LwM2MTransportConfigServer { } return FULL_FILE_PATH.toUri().getPath(); } - - public ResourceModel getResourceModel(Registration registration, LwM2mPath pathIds) { - return this.modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()); - } - - public ResourceModel.Type getResourceModelType(Registration registration, LwM2mPath pathIds) { - ResourceModel resource = this.getResourceModel(registration, pathIds); - return (resource == null) ? null : resource.type; - } - - public ResourceModel.Operations getOperation(Registration registration, LwM2mPath pathIds) { - ResourceModel resource = this.getResourceModel(registration, pathIds); - return (resource == null) ? ResourceModel.Operations.NONE : resource.operations; - } - } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportResourceCache.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportResourceCache.java index 5160479d1e..83439530c0 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportResourceCache.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportResourceCache.java @@ -19,8 +19,8 @@ import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.Resource; import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.transport.TransportResourceCache; import org.thingsboard.server.common.transport.TransportService; @@ -42,8 +42,8 @@ import java.util.concurrent.locks.ReentrantLock; public class DefaultTransportResourceCache implements TransportResourceCache { private final Lock resourceFetchLock = new ReentrantLock(); - private final ConcurrentMap resources = new ConcurrentHashMap<>(); - private final Set keys = ConcurrentHashMap.newKeySet(); + private final ConcurrentMap resources = new ConcurrentHashMap<>(); + private final Set keys = ConcurrentHashMap.newKeySet(); private final DataDecodingEncodingService dataDecodingEncodingService; private final TransportService transportService; @@ -53,49 +53,49 @@ public class DefaultTransportResourceCache implements TransportResourceCache { } @Override - public Resource get(TenantId tenantId, ResourceType resourceType, String resourceId) { - ResourceKey resourceKey = new ResourceKey(tenantId, resourceType, resourceId); - Resource resource; + public Optional get(TenantId tenantId, ResourceType resourceType, String resourceKey) { + ResourceCompositeKey compositeKey = new ResourceCompositeKey(tenantId, resourceType, resourceKey); + TbResource resource; - if (keys.contains(resourceKey)) { - resource = resources.get(resourceKey); + if (keys.contains(compositeKey)) { + resource = resources.get(compositeKey); if (resource == null) { - resource = resources.get(resourceKey.getSystemKey()); + resource = resources.get(compositeKey.getSystemKey()); } } else { resourceFetchLock.lock(); try { - if (keys.contains(resourceKey)) { - resource = resources.get(resourceKey); + if (keys.contains(compositeKey)) { + resource = resources.get(compositeKey); if (resource == null) { - resource = resources.get(resourceKey.getSystemKey()); + resource = resources.get(compositeKey.getSystemKey()); } } else { - resource = fetchResource(resourceKey); - keys.add(resourceKey); + resource = fetchResource(compositeKey); + keys.add(compositeKey); } } finally { resourceFetchLock.unlock(); } } - return resource; + return Optional.ofNullable(resource); } - private Resource fetchResource(ResourceKey resourceKey) { - UUID tenantId = resourceKey.getTenantId().getId(); + private TbResource fetchResource(ResourceCompositeKey compositeKey) { + UUID tenantId = compositeKey.getTenantId().getId(); TransportProtos.GetResourceRequestMsg.Builder builder = TransportProtos.GetResourceRequestMsg.newBuilder(); builder .setTenantIdLSB(tenantId.getLeastSignificantBits()) .setTenantIdMSB(tenantId.getMostSignificantBits()) - .setResourceType(resourceKey.resourceType.name()) - .setResourceId(resourceKey.resourceId); + .setResourceType(compositeKey.resourceType.name()) + .setResourceKey(compositeKey.resourceKey); TransportProtos.GetResourceResponseMsg responseMsg = transportService.getResource(builder.build()); - Optional optionalResource = dataDecodingEncodingService.decode(responseMsg.getResource().toByteArray()); + Optional optionalResource = dataDecodingEncodingService.decode(responseMsg.getResource().toByteArray()); if (optionalResource.isPresent()) { - Resource resource = optionalResource.get(); - resources.put(new ResourceKey(resource.getTenantId(), resource.getResourceType(), resource.getResourceId()), resource); + TbResource resource = optionalResource.get(); + resources.put(new ResourceCompositeKey(resource.getTenantId(), resource.getResourceType(), resource.getResourceKey()), resource); return resource; } @@ -103,28 +103,28 @@ public class DefaultTransportResourceCache implements TransportResourceCache { } @Override - public void update(TenantId tenantId, ResourceType resourceType, String resourceId) { - ResourceKey resourceKey = new ResourceKey(tenantId, resourceType, resourceId); - if (keys.contains(resourceKey) || resources.containsKey(resourceKey)) { - fetchResource(resourceKey); + public void update(TenantId tenantId, ResourceType resourceType, String resourceKey) { + ResourceCompositeKey compositeKey = new ResourceCompositeKey(tenantId, resourceType, resourceKey); + if (keys.contains(compositeKey) || resources.containsKey(compositeKey)) { + fetchResource(compositeKey); } } @Override - public void evict(TenantId tenantId, ResourceType resourceType, String resourceId) { - ResourceKey resourceKey = new ResourceKey(tenantId, resourceType, resourceId); - keys.remove(resourceKey); - resources.remove(resourceKey); + public void evict(TenantId tenantId, ResourceType resourceType, String resourceKey) { + ResourceCompositeKey compositeKey = new ResourceCompositeKey(tenantId, resourceType, resourceKey); + keys.remove(compositeKey); + resources.remove(compositeKey); } @Data - private static class ResourceKey { + private static class ResourceCompositeKey { private final TenantId tenantId; private final ResourceType resourceType; - private final String resourceId; + private final String resourceKey; - public ResourceKey getSystemKey() { - return new ResourceKey(TenantId.SYS_TENANT_ID, resourceType, resourceId); + public ResourceCompositeKey getSystemKey() { + return new ResourceCompositeKey(TenantId.SYS_TENANT_ID, resourceType, resourceKey); } } } 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 6a8526f543..740a2a622f 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 @@ -288,7 +288,7 @@ public class DefaultTransportService implements TransportService { } @Override - public void process(TransportProtos.ValidateDeviceLwM2MCredentialsRequestMsg msg, TransportServiceCallback callback) { + 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), @@ -708,14 +708,23 @@ public class DefaultTransportService implements TransportService { TransportProtos.ResourceUpdateMsg msg = toSessionMsg.getResourceUpdateMsg(); TenantId tenantId = new TenantId(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB())); ResourceType resourceType = ResourceType.valueOf(msg.getResourceType()); - String resourceId = msg.getResourceId(); + String resourceId = msg.getResourceKey(); transportResourceCache.update(tenantId, resourceType, resourceId); + sessions.forEach((id, mdRez) -> { + log.warn("ResourceUpdate - [{}] [{}]", id, mdRez); + transportCallbackExecutor.submit(() -> mdRez.getListener().onResourceUpdate(Optional.ofNullable(msg))); + }); + } else if (toSessionMsg.hasResourceDeleteMsg()) { TransportProtos.ResourceDeleteMsg msg = toSessionMsg.getResourceDeleteMsg(); TenantId tenantId = new TenantId(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB())); ResourceType resourceType = ResourceType.valueOf(msg.getResourceType()); - String resourceId = msg.getResourceId(); + String resourceId = msg.getResourceKey(); transportResourceCache.evict(tenantId, resourceType, resourceId); + sessions.forEach((id, mdRez) -> { + log.warn("ResourceDelete - [{}] [{}]", id, mdRez); + transportCallbackExecutor.submit(() -> mdRez.getListener().onResourceDelete(Optional.ofNullable(msg))); + }); } else { //TODO: should we notify the device actor about missed session? log.debug("[{}] Missing session.", sessionId); @@ -827,7 +836,7 @@ public class DefaultTransportService implements TransportService { } private void sendToRuleEngine(TenantId tenantId, DeviceId deviceId, TransportProtos.SessionInfoProto sessionInfo, JsonObject json, - TbMsgMetaData metaData, SessionMsgType sessionMsgType, TbQueueCallback callback) { + TbMsgMetaData metaData, SessionMsgType sessionMsgType, TbQueueCallback callback) { DeviceProfileId deviceProfileId = new DeviceProfileId(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB())); DeviceProfile deviceProfile = deviceProfileCache.get(deviceProfileId); RuleChainId ruleChainId; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/SslUtil.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java similarity index 93% rename from common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/SslUtil.java rename to common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java index f376077b84..77e4045655 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/SslUtil.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java @@ -13,13 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.transport.mqtt.util; +package org.thingsboard.server.common.transport.util; import lombok.extern.slf4j.Slf4j; import org.springframework.util.Base64Utils; import org.thingsboard.server.common.msg.EncryptionUtil; -import java.io.IOException; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; diff --git a/dao/pom.xml b/dao/pom.xml index e1a08cb74d..9ba3033970 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -111,6 +111,14 @@ com.fasterxml.jackson.core jackson-databind + + org.hibernate.validator + hibernate-validator + + + org.glassfish + javax.el + org.springframework spring-context @@ -198,6 +206,11 @@ hsqldb test + + org.junit.jupiter + junit-jupiter-params + test + org.springframework spring-context-support @@ -214,6 +227,10 @@ org.elasticsearch.client rest + + org.eclipse.leshan + leshan-core + diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesServiceImpl.java index 4e9c805454..76b7f1de31 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesServiceImpl.java @@ -26,6 +26,7 @@ import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.CustomerId; @@ -35,9 +36,11 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.claim.ClaimData; import org.thingsboard.server.dao.device.claim.ClaimResponse; import org.thingsboard.server.dao.device.claim.ClaimResult; +import org.thingsboard.server.dao.device.claim.ReclaimResult; import org.thingsboard.server.dao.model.ModelConstants; import java.io.IOException; @@ -62,6 +65,8 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService { @Autowired private AttributesService attributesService; @Autowired + private CustomerService customerService; + @Autowired private CacheManager cacheManager; @Value("${security.claim.allowClaimingByDefault}") @@ -158,21 +163,22 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService { } @Override - public ListenableFuture> reClaimDevice(TenantId tenantId, Device device) { + public ListenableFuture reClaimDevice(TenantId tenantId, Device device) { if (!device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { cacheEviction(device.getId()); - + Customer unassignedCustomer = customerService.findCustomerById(tenantId, device.getCustomerId()); device.setCustomerId(null); deviceService.saveDevice(device); if (isAllowedClaimingByDefault) { - return Futures.immediateFuture(Collections.emptyList()); + return Futures.immediateFuture(new ReclaimResult(unassignedCustomer)); } - return attributesService.save(tenantId, device.getId(), DataConstants.SERVER_SCOPE, Collections.singletonList( - new BaseAttributeKvEntry(new BooleanDataEntry(CLAIM_ATTRIBUTE_NAME, true), - System.currentTimeMillis()))); + return Futures.transform(attributesService.save( + tenantId, device.getId(), DataConstants.SERVER_SCOPE, Collections.singletonList( + new BaseAttributeKvEntry(new BooleanDataEntry(CLAIM_ATTRIBUTE_NAME, true), System.currentTimeMillis()) + )), result -> new ReclaimResult(unassignedCustomer), MoreExecutors.directExecutor()); } cacheEviction(device.getId()); - return Futures.immediateFuture(Collections.emptyList()); + return Futures.immediateFuture(new ReclaimResult(null)); } private List constructCacheKey(DeviceId deviceId) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 5276b20832..c47e4014e7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -464,8 +464,10 @@ public class ModelConstants { public static final String RESOURCE_TABLE_NAME = "resource"; public static final String RESOURCE_TENANT_ID_COLUMN = TENANT_ID_COLUMN; public static final String RESOURCE_TYPE_COLUMN = "resource_type"; - public static final String RESOURCE_ID_COLUMN = "resource_id"; - public static final String RESOURCE_VALUE_COLUMN = "resource_value"; + public static final String RESOURCE_KEY_COLUMN = "resource_key"; + public static final String RESOURCE_TITLE_COLUMN = TITLE_PROPERTY; + public static final String RESOURCE_FILE_NAME_COLUMN = "file_name"; + public static final String RESOURCE_DATA_COLUMN = "data"; /** * Edge constants. diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ResourceCompositeKey.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ResourceCompositeKey.java deleted file mode 100644 index 6592cf757d..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ResourceCompositeKey.java +++ /dev/null @@ -1,44 +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.dao.model.sql; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.thingsboard.server.common.data.Resource; - -import javax.persistence.Transient; -import java.io.Serializable; -import java.util.UUID; - -@NoArgsConstructor -@AllArgsConstructor -@Data -public class ResourceCompositeKey implements Serializable { - - @Transient - private static final long serialVersionUID = -3789469030818742769L; - - private UUID tenantId; - private String resourceType; - private String resourceId; - - public ResourceCompositeKey(Resource resource) { - this.tenantId = resource.getTenantId().getId(); - this.resourceType = resource.getResourceType().name(); - this.resourceId = resource.getResourceId(); - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java new file mode 100644 index 0000000000..5b22b6e012 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java @@ -0,0 +1,105 @@ +/** + * 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.dao.model.sql; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.id.TbResourceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.SearchTextEntity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_DATA_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_FILE_NAME_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TABLE_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TENANT_ID_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TITLE_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TYPE_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@Table(name = RESOURCE_TABLE_NAME) +public class TbResourceEntity extends BaseSqlEntity implements SearchTextEntity { + + @Column(name = RESOURCE_TENANT_ID_COLUMN, columnDefinition = "uuid") + private UUID tenantId; + + @Column(name = RESOURCE_TITLE_COLUMN) + private String title; + + @Column(name = RESOURCE_TYPE_COLUMN) + private String resourceType; + + @Column(name = RESOURCE_KEY_COLUMN) + private String resourceKey; + + @Column(name = SEARCH_TEXT_PROPERTY) + private String searchText; + + @Column(name = RESOURCE_FILE_NAME_COLUMN) + private String fileName; + + @Column(name = RESOURCE_DATA_COLUMN) + private String data; + + public TbResourceEntity() { + } + + public TbResourceEntity(TbResource resource) { + if (resource.getId() != null) { + this.id = resource.getId().getId(); + } + this.createdTime = resource.getCreatedTime(); + if (resource.getTenantId() != null) { + this.tenantId = resource.getTenantId().getId(); + } + this.title = resource.getTitle(); + this.resourceType = resource.getResourceType().name(); + this.resourceKey = resource.getResourceKey(); + this.searchText = resource.getSearchText(); + this.fileName = resource.getFileName(); + this.data = resource.getData(); + } + + @Override + public TbResource toData() { + TbResource resource = new TbResource(new TbResourceId(id)); + resource.setCreatedTime(createdTime); + resource.setTenantId(new TenantId(tenantId)); + resource.setTitle(title); + resource.setResourceType(ResourceType.valueOf(resourceType)); + resource.setResourceKey(resourceKey); + resource.setSearchText(searchText); + resource.setFileName(fileName); + resource.setData(data); + return resource; + } + + @Override + public String getSearchTextSource() { + return this.searchText; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ResourceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java similarity index 54% rename from dao/src/main/java/org/thingsboard/server/dao/model/sql/ResourceEntity.java rename to dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java index fdb24e6159..78c36861c4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ResourceEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java @@ -16,62 +16,76 @@ package org.thingsboard.server.dao.model.sql; import lombok.Data; -import org.thingsboard.server.common.data.Resource; +import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.model.ToData; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.SearchTextEntity; import javax.persistence.Column; import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.IdClass; import javax.persistence.Table; import java.util.UUID; -import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_ID_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TABLE_NAME; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TENANT_ID_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TITLE_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TYPE_COLUMN; -import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_VALUE_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; @Data +@EqualsAndHashCode(callSuper = true) @Entity @Table(name = RESOURCE_TABLE_NAME) -@IdClass(ResourceCompositeKey.class) -public class ResourceEntity implements ToData { +public class TbResourceInfoEntity extends BaseSqlEntity implements SearchTextEntity { - @Id @Column(name = RESOURCE_TENANT_ID_COLUMN, columnDefinition = "uuid") private UUID tenantId; - @Id + @Column(name = RESOURCE_TITLE_COLUMN) + private String title; + @Column(name = RESOURCE_TYPE_COLUMN) private String resourceType; - @Id - @Column(name = RESOURCE_ID_COLUMN) - private String resourceId; + @Column(name = RESOURCE_KEY_COLUMN) + private String resourceKey; - @Column(name = RESOURCE_VALUE_COLUMN) - private String value; + @Column(name = SEARCH_TEXT_PROPERTY) + private String searchText; - public ResourceEntity() { + public TbResourceInfoEntity() { } - public ResourceEntity(Resource resource) { + public TbResourceInfoEntity(TbResourceInfo resource) { + if (resource.getId() != null) { + this.id = resource.getId().getId(); + } + this.createdTime = resource.getCreatedTime(); this.tenantId = resource.getTenantId().getId(); + this.title = resource.getTitle(); this.resourceType = resource.getResourceType().name(); - this.resourceId = resource.getResourceId(); - this.value = resource.getValue(); + this.resourceKey = resource.getResourceKey(); + this.searchText = resource.getSearchText(); } @Override - public Resource toData() { - Resource resource = new Resource(); + public TbResourceInfo toData() { + TbResourceInfo resource = new TbResourceInfo(new TbResourceId(id)); + resource.setCreatedTime(createdTime); resource.setTenantId(new TenantId(tenantId)); + resource.setTitle(title); resource.setResourceType(ResourceType.valueOf(resourceType)); - resource.setResourceId(resourceId); - resource.setValue(value); + resource.setResourceKey(resourceKey); + resource.setSearchText(searchText); return resource; } + + @Override + public String getSearchTextSource() { + return title; + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java deleted file mode 100644 index a451a4e627..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java +++ /dev/null @@ -1,106 +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.dao.resource; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.Resource; -import org.thingsboard.server.common.data.ResourceType; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.dao.exception.DataValidationException; - -import java.util.List; - -import static org.thingsboard.server.dao.device.DeviceServiceImpl.INCORRECT_TENANT_ID; -import static org.thingsboard.server.dao.service.Validator.validateId; - -@Service -@Slf4j -public class BaseResourceService implements ResourceService { - - private final ResourceDao resourceDao; - - public BaseResourceService(ResourceDao resourceDao) { - this.resourceDao = resourceDao; - } - - @Override - public Resource saveResource(Resource resource) { - log.trace("Executing saveResource [{}]", resource); - validate(resource); - return resourceDao.saveResource(resource); - } - - @Override - public Resource getResource(TenantId tenantId, ResourceType resourceType, String resourceId) { - log.trace("Executing getResource [{}] [{}] [{}]", tenantId, resourceType, resourceId); - validate(tenantId, resourceType, resourceId); - return resourceDao.getResource(tenantId, resourceType, resourceId); - } - - @Override - public void deleteResource(TenantId tenantId, ResourceType resourceType, String resourceId) { - log.trace("Executing deleteResource [{}] [{}] [{}]", tenantId, resourceType, resourceId); - validate(tenantId, resourceType, resourceId); - resourceDao.deleteResource(tenantId, resourceType, resourceId); - } - - @Override - public PageData findResourcesByTenantId(TenantId tenantId, PageLink pageLink) { - log.trace("Executing findByTenantId [{}]", tenantId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - return resourceDao.findAllByTenantId(tenantId, pageLink); - } - - - @Override - public List findResourcesByTenantIdResourceType(TenantId tenantId, ResourceType resourceType) { - log.trace("Executing findByTenantId [{}]", tenantId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - return resourceDao.findAllByTenantIdResourceType(tenantId, resourceType); - } - - @Override - public void deleteResourcesByTenantId(TenantId tenantId) { - log.trace("Executing deleteDevicesByTenantId, tenantId [{}]", tenantId); - validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - resourceDao.removeAllByTenantId(tenantId); - } - - protected void validate(Resource resource) { - if (resource == null) { - throw new DataValidationException("Resource should be specified!"); - } - - if (resource.getValue() == null) { - throw new DataValidationException("Resource value should be specified!"); - } - validate(resource.getTenantId(), resource.getResourceType(), resource.getResourceId()); - } - - protected void validate(TenantId tenantId, ResourceType resourceType, String resourceId) { - if (resourceType == null) { - throw new DataValidationException("Resource type should be specified!"); - } - if (resourceId == null) { - throw new DataValidationException("Resource id should be specified!"); - } - validateId(tenantId, "Incorrect tenantId "); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseTbResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseTbResourceService.java new file mode 100644 index 0000000000..0df2f19e80 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseTbResourceService.java @@ -0,0 +1,291 @@ +/** + * 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.dao.resource; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.leshan.core.model.DDFFileParser; +import org.eclipse.leshan.core.model.DefaultDDFFileValidator; +import org.eclipse.leshan.core.model.InvalidDDFFileException; +import org.eclipse.leshan.core.model.ObjectModel; +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.id.TbResourceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.lwm2m.LwM2mInstance; +import org.thingsboard.server.common.data.lwm2m.LwM2mObject; +import org.thingsboard.server.common.data.lwm2m.LwM2mResourceObserve; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.service.PaginatedRemover; +import org.thingsboard.server.dao.service.Validator; +import org.thingsboard.server.dao.tenant.TenantDao; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_KEY; +import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_SEARCH_TEXT; +import static org.thingsboard.server.dao.device.DeviceServiceImpl.INCORRECT_TENANT_ID; +import static org.thingsboard.server.dao.service.Validator.validateId; + +@Service +@Slf4j +public class BaseTbResourceService implements TbResourceService { + + public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId "; + private final TbResourceDao resourceDao; + private final TbResourceInfoDao resourceInfoDao; + private final TenantDao tenantDao; + private final DDFFileParser ddfFileParser; + + public BaseTbResourceService(TbResourceDao resourceDao, TbResourceInfoDao resourceInfoDao, TenantDao tenantDao) { + this.resourceDao = resourceDao; + this.resourceInfoDao = resourceInfoDao; + this.tenantDao = tenantDao; + this.ddfFileParser = new DDFFileParser(new DefaultDDFFileValidator()); + } + + @Override + public TbResource saveResource(TbResource resource) throws InvalidDDFFileException, IOException { + log.trace("Executing saveResource [{}]", resource); + if (StringUtils.isEmpty(resource.getData())) { + throw new DataValidationException("Resource data should be specified!"); + } + if (ResourceType.LWM2M_MODEL.equals(resource.getResourceType())) { + List objectModels = + ddfFileParser.parseEx(new ByteArrayInputStream(Base64.getDecoder().decode(resource.getData())), resource.getSearchText()); + if (!objectModels.isEmpty()) { + ObjectModel objectModel = objectModels.get(0); + + String resourceKey = objectModel.id + LWM2M_SEPARATOR_KEY + objectModel.getVersion(); + String name = objectModel.name; + resource.setResourceKey(resourceKey); + if (resource.getId() == null) { + resource.setTitle(name + " id=" + objectModel.id + " v" + objectModel.getVersion()); + } + resource.setSearchText(resourceKey + LWM2M_SEPARATOR_SEARCH_TEXT + name); + } else { + throw new DataValidationException(String.format("Could not parse the XML of objectModel with name %s", resource.getSearchText())); + } + } else { + resource.setResourceKey(resource.getFileName()); + } + + resourceValidator.validate(resource, TbResourceInfo::getTenantId); + + try { + return resourceDao.save(resource.getTenantId(), resource); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("resource_unq_key")) { + String field = ResourceType.LWM2M_MODEL.equals(resource.getResourceType()) ? "resourceKey" : "fileName"; + throw new DataValidationException("Resource with such " + field + " already exists!"); + } else { + throw t; + } + } + + } + + @Override + public TbResource getResource(TenantId tenantId, ResourceType resourceType, String resourceKey) { + log.trace("Executing getResource [{}] [{}] [{}]", tenantId, resourceType, resourceKey); + return resourceDao.getResource(tenantId, resourceType, resourceKey); + } + + @Override + public TbResource findResourceById(TenantId tenantId, TbResourceId resourceId) { + log.trace("Executing findResourceById [{}] [{}]", tenantId, resourceId); + Validator.validateId(resourceId, INCORRECT_RESOURCE_ID + resourceId); + return resourceDao.findById(tenantId, resourceId.getId()); + } + + @Override + public TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId) { + log.trace("Executing findResourceInfoById [{}] [{}]", tenantId, resourceId); + Validator.validateId(resourceId, INCORRECT_RESOURCE_ID + resourceId); + return resourceInfoDao.findById(tenantId, resourceId.getId()); + } + + @Override + public void deleteResource(TenantId tenantId, TbResourceId resourceId) { + log.trace("Executing deleteResource [{}] [{}]", tenantId, resourceId); + Validator.validateId(resourceId, INCORRECT_RESOURCE_ID + resourceId); + resourceDao.removeById(tenantId, resourceId.getId()); + } + + @Override + public PageData findAllTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findAllTenantResourcesByTenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + return resourceInfoDao.findAllTenantResourcesByTenantId(tenantId.getId(), pageLink); + } + + @Override + public PageData findTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findTenantResourcesByTenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + return resourceInfoDao.findTenantResourcesByTenantId(tenantId.getId(), pageLink); + } + + @Override + public List findLwM2mObjectPage(TenantId tenantId, String sortProperty, String sortOrder, PageLink pageLink) { + log.trace("Executing findByTenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + PageData resourcePageData = resourceDao.findResourcesByTenantIdAndResourceType( + tenantId, + ResourceType.LWM2M_MODEL, pageLink); + return resourcePageData.getData().stream() + .map(this::toLwM2mObject) + .sorted(getComparator(sortProperty, sortOrder)) + .collect(Collectors.toList()); + } + + @Override + public List findLwM2mObject(TenantId tenantId, String sortOrder, + String sortProperty, + String[] objectIds) { + log.trace("Executing findByTenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + List resources = resourceDao.findResourcesByTenantIdAndResourceType(tenantId, ResourceType.LWM2M_MODEL, + objectIds, + null); + return resources.stream() + .map(this::toLwM2mObject) + .sorted(getComparator(sortProperty, sortOrder)) + .collect(Collectors.toList()); + } + + @Override + public void deleteResourcesByTenantId(TenantId tenantId) { + log.trace("Executing deleteResourcesByTenantId, tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + tenantResourcesRemover.removeEntities(tenantId, tenantId); + } + + private LwM2mObject toLwM2mObject(TbResource resource) { + try { + DDFFileParser ddfFileParser = new DDFFileParser(new DefaultDDFFileValidator()); + List objectModels = + ddfFileParser.parseEx(new ByteArrayInputStream(Base64.getDecoder().decode(resource.getData())), resource.getSearchText()); + if (objectModels.size() == 0) { + return null; + } else { + ObjectModel obj = objectModels.get(0); + LwM2mObject lwM2mObject = new LwM2mObject(); + lwM2mObject.setId(obj.id); + lwM2mObject.setKeyId(resource.getResourceKey()); + lwM2mObject.setName(obj.name); + lwM2mObject.setMultiple(obj.multiple); + lwM2mObject.setMandatory(obj.mandatory); + LwM2mInstance instance = new LwM2mInstance(); + instance.setId(0); + List resources = new ArrayList<>(); + obj.resources.forEach((k, v) -> { + if (!v.operations.isExecutable()) { + LwM2mResourceObserve lwM2MResourceObserve = new LwM2mResourceObserve(k, v.name, false, false, false); + resources.add(lwM2MResourceObserve); + } + }); + instance.setResources(resources.toArray(LwM2mResourceObserve[]::new)); + lwM2mObject.setInstances(new LwM2mInstance[]{instance}); + return lwM2mObject; + } + } catch (IOException | InvalidDDFFileException e) { + log.error("Could not parse the XML of objectModel with name [{}]", resource.getSearchText(), e); + return null; + } + } + + private Comparator getComparator(String sortProperty, String sortOrder) { + Comparator comparator; + if ("name".equals(sortProperty)) { + comparator = Comparator.comparing(LwM2mObject::getName); + } else { + comparator = Comparator.comparingLong(LwM2mObject::getId); + } + return "DESC".equals(sortOrder) ? comparator.reversed() : comparator; + } + + private DataValidator resourceValidator = new DataValidator<>() { + + @Override + protected void validateDataImpl(TenantId tenantId, TbResource resource) { + if (StringUtils.isEmpty(resource.getTitle())) { + throw new DataValidationException("Resource title should be specified!"); + } + if (resource.getResourceType() == null) { + throw new DataValidationException("Resource type should be specified!"); + } + if (StringUtils.isEmpty(resource.getFileName())) { + throw new DataValidationException("Resource file name should be specified!"); + } + if (StringUtils.isEmpty(resource.getResourceKey())) { + throw new DataValidationException("Resource key should be specified!"); + } + if (resource.getTenantId() == null) { + resource.setTenantId(new TenantId(ModelConstants.NULL_UUID)); + } + if (!resource.getTenantId().getId().equals(ModelConstants.NULL_UUID)) { + Tenant tenant = tenantDao.findById(tenantId, resource.getTenantId().getId()); + if (tenant == null) { + throw new DataValidationException("Resource is referencing to non-existent tenant!"); + } + } + if (resource.getResourceType().equals(ResourceType.LWM2M_MODEL) && toLwM2mObject(resource) == null) { + throw new DataValidationException(String.format("Could not parse the XML of objectModel with name %s", resource.getSearchText())); + } + } + }; + + private PaginatedRemover tenantResourcesRemover = + new PaginatedRemover<>() { + + @Override + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { + return resourceDao.findAllByTenantId(id, pageLink); + } + + @Override + protected void removeEntity(TenantId tenantId, TbResource entity) { + deleteResource(tenantId, new TbResourceId(entity.getUuidId())); + } + }; + + protected Optional extractConstraintViolationException(Exception t) { + if (t instanceof ConstraintViolationException) { + return Optional.of((ConstraintViolationException) t); + } else if (t.getCause() instanceof ConstraintViolationException) { + return Optional.of((ConstraintViolationException) (t.getCause())); + } else { + return Optional.empty(); + } + } +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java similarity index 50% rename from common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java rename to dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java index 8d84af6e6c..230e104191 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java @@ -15,25 +15,27 @@ */ package org.thingsboard.server.dao.resource; -import org.thingsboard.server.common.data.Resource; import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.Dao; import java.util.List; +public interface TbResourceDao extends Dao { -public interface ResourceService { - Resource saveResource(Resource resource); + TbResource getResource(TenantId tenantId, ResourceType resourceType, String resourceId); - Resource getResource(TenantId tenantId, ResourceType resourceType, String resourceId); + PageData findAllByTenantId(TenantId tenantId, PageLink pageLink); - PageData findResourcesByTenantId(TenantId tenantId, PageLink pageLink); + PageData findResourcesByTenantIdAndResourceType(TenantId tenantId, + ResourceType resourceType, + PageLink pageLink); - List findResourcesByTenantIdResourceType(TenantId tenantId, ResourceType resourceType); - - void deleteResource(TenantId tenantId, ResourceType resourceType, String resourceId); - - void deleteResourcesByTenantId(TenantId tenantId); + List findResourcesByTenantIdAndResourceType(TenantId tenantId, + ResourceType resourceType, + String[] objectIds, + String searchText); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/ResourceDao.java b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java similarity index 53% rename from dao/src/main/java/org/thingsboard/server/dao/resource/ResourceDao.java rename to dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java index 68ad4db4fb..f54e53b849 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/ResourceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java @@ -15,26 +15,17 @@ */ package org.thingsboard.server.dao.resource; -import org.thingsboard.server.common.data.Resource; -import org.thingsboard.server.common.data.ResourceType; -import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.Dao; -import java.util.List; +import java.util.UUID; -public interface ResourceDao { +public interface TbResourceInfoDao extends Dao { - Resource saveResource(Resource resource); + PageData findAllTenantResourcesByTenantId(UUID tenantId, PageLink pageLink); - Resource getResource(TenantId tenantId, ResourceType resourceType, String resourceId); + PageData findTenantResourcesByTenantId(UUID tenantId, PageLink pageLink); - void deleteResource(TenantId tenantId, ResourceType resourceType, String resourceId); - - PageData findAllByTenantId(TenantId tenantId, PageLink pageLink); - - - List findAllByTenantIdResourceType(TenantId tenantId, ResourceType resourceType); - - void removeAllByTenantId(TenantId tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java index e8b8eaf009..ff8e79efc2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java @@ -17,29 +17,50 @@ package org.thingsboard.server.dao.service; import com.fasterxml.jackson.databind.JsonNode; import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.hibernate.validator.HibernateValidatorConfiguration; +import org.hibernate.validator.cfg.ConstraintMapping; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.dao.TenantEntityDao; import org.thingsboard.server.dao.exception.DataValidationException; +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.Set; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; @Slf4j public abstract class DataValidator> { private static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$", Pattern.CASE_INSENSITIVE); + private static Validator fieldsValidator; + + static { + initializeFieldsValidator(); + } + public void validate(D data, Function tenantIdFunction) { try { if (data == null) { throw new DataValidationException("Data object can't be null!"); } + + List validationErrors = validateFields(data); + if (!validationErrors.isEmpty()) { + throw new IllegalArgumentException("Validation error: " + String.join(", ", validationErrors)); + } + TenantId tenantId = tenantIdFunction.apply(data); validateDataImpl(tenantId, data); if (data.getId() == null) { @@ -81,6 +102,14 @@ public abstract class DataValidator> { return emailMatcher.matches(); } + private List validateFields(D data) { + Set> constraintsViolations = fieldsValidator.validate(data); + return constraintsViolations.stream() + .map(ConstraintViolation::getMessage) + .distinct() + .collect(Collectors.toList()); + } + protected void validateNumberOfEntitiesPerTenant(TenantId tenantId, TenantEntityDao tenantEntityDao, long maxEntities, @@ -111,4 +140,13 @@ public abstract class DataValidator> { throw new DataValidationException("Provided json structure is different from stored one '" + actualNode + "'!"); } } + + private static void initializeFieldsValidator() { + HibernateValidatorConfiguration validatorConfiguration = Validation.byProvider(HibernateValidator.class).configure(); + ConstraintMapping constraintMapping = validatorConfiguration.createConstraintMapping(); + constraintMapping.constraintDefinition(NoXss.class).validatedBy(NoXssValidator.class); + validatorConfiguration.addMapping(constraintMapping); + + fieldsValidator = validatorConfiguration.buildValidatorFactory().getValidator(); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/NoXssValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/NoXssValidator.java new file mode 100644 index 0000000000..e16aebbfea --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/NoXssValidator.java @@ -0,0 +1,57 @@ +/** + * 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.dao.service; + +import com.google.common.io.Resources; +import lombok.extern.slf4j.Slf4j; +import org.owasp.validator.html.AntiSamy; +import org.owasp.validator.html.Policy; +import org.owasp.validator.html.PolicyException; +import org.owasp.validator.html.ScanException; +import org.thingsboard.server.common.data.validation.NoXss; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +@Slf4j +public class NoXssValidator implements ConstraintValidator { + private static final AntiSamy xssChecker = new AntiSamy(); + private static Policy xssPolicy; + + @Override + public void initialize(NoXss constraintAnnotation) { + if (xssPolicy == null) { + try { + xssPolicy = Policy.getInstance(Resources.getResource("xss-policy.xml")); + } catch (Exception e) { + log.error("Failed to set xss policy: {}", e.getMessage()); + } + } + } + + @Override + public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) { + if (!(value instanceof String) || ((String) value).isEmpty() || xssPolicy == null) { + return true; + } + + try { + return xssChecker.scan((String) value, xssPolicy).getNumberOfErrors() == 0; + } catch (ScanException | PolicyException e) { + return false; + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java index 9ed515f518..a759d0de74 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java @@ -16,7 +16,7 @@ package org.thingsboard.server.dao.sql.query; import lombok.Data; -import org.springframework.util.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; @@ -42,7 +42,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -245,8 +244,9 @@ public class EntityKeyMapping { entityTypeStr = "'" + entityType.name() + "'"; } ctx.addStringParameter(alias + "_key_id", entityKey.getKey()); - String filterQuery = toQueries(ctx, entityFilter.getType()).filter(Objects::nonNull).collect( - Collectors.joining(" and ")); + String filterQuery = toQueries(ctx, entityFilter.getType()) + .filter(StringUtils::isNotEmpty) + .collect(Collectors.joining(" and ")); if (StringUtils.isEmpty(filterQuery)) { filterQuery = ""; } else { @@ -293,8 +293,10 @@ public class EntityKeyMapping { } public static String buildQuery(QueryContext ctx, List mappings, EntityFilterType filterType) { - return mappings.stream().flatMap(mapping -> mapping.toQueries(ctx, filterType)).filter(Objects::nonNull).collect( - Collectors.joining(" AND ")); + return mappings.stream() + .flatMap(mapping -> mapping.toQueries(ctx, filterType)) + .filter(StringUtils::isNotEmpty) + .collect(Collectors.joining(" AND ")); } public static List prepareKeyMapping(EntityDataQuery query) { @@ -461,9 +463,8 @@ public class EntityKeyMapping { ComplexFilterPredicate predicate, EntityFilterType filterType) { String result = predicate.getPredicates().stream() .map(keyFilterPredicate -> this.buildPredicateQuery(ctx, alias, key, keyFilterPredicate, filterType)) - .filter(Objects::nonNull).collect(Collectors.joining( - " " + predicate.getOperation().name() + " " - )); + .filter(StringUtils::isNotEmpty) + .collect(Collectors.joining(" " + predicate.getOperation().name() + " ")); if (!result.trim().isEmpty()) { result = "( " + result + " )"; } @@ -520,7 +521,7 @@ public class EntityKeyMapping { String paramName = getNextParameterName(field); String value = stringFilterPredicate.getValue().getValue(); if (value.isEmpty()) { - return null; + return ""; } String stringOperationQuery = ""; if (stringFilterPredicate.isIgnoreCase()) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java new file mode 100644 index 0000000000..f35f654f77 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java @@ -0,0 +1,95 @@ +/** + * 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.dao.sql.resource; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.TbResourceEntity; +import org.thingsboard.server.dao.resource.TbResourceDao; +import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; + +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +@Slf4j +@Component +public class JpaTbResourceDao extends JpaAbstractSearchTextDao implements TbResourceDao { + + private final TbResourceRepository resourceRepository; + + public JpaTbResourceDao(TbResourceRepository resourceRepository) { + this.resourceRepository = resourceRepository; + } + + @Override + protected Class getEntityClass() { + return TbResourceEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return resourceRepository; + } + + @Override + public TbResource getResource(TenantId tenantId, ResourceType resourceType, String resourceKey) { + + return DaoUtil.getData(resourceRepository.findByTenantIdAndResourceTypeAndResourceKey(tenantId.getId(), resourceType.name(), resourceKey)); + } + + @Override + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(resourceRepository.findAllByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findResourcesByTenantIdAndResourceType(TenantId tenantId, + ResourceType resourceType, + PageLink pageLink) { + return DaoUtil.toPageData(resourceRepository.findResourcesPage( + tenantId.getId(), + TenantId.SYS_TENANT_ID.getId(), + resourceType.name(), + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink) + )); + } + + @Override + public List findResourcesByTenantIdAndResourceType(TenantId tenantId, ResourceType resourceType, + String[] objectIds, + String searchText) { + return objectIds == null ? + DaoUtil.convertDataList(resourceRepository.findResources( + tenantId.getId(), + TenantId.SYS_TENANT_ID.getId(), + resourceType.name(), + Objects.toString(searchText, ""))) : + DaoUtil.convertDataList(resourceRepository.findResourcesByIds( + tenantId.getId(), + TenantId.SYS_TENANT_ID.getId(), + resourceType.name(), objectIds)); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java new file mode 100644 index 0000000000..9e4aac6a70 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java @@ -0,0 +1,69 @@ +/** + * 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.dao.sql.resource; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.TbResourceInfoEntity; +import org.thingsboard.server.dao.resource.TbResourceInfoDao; +import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; + +import java.util.Objects; +import java.util.UUID; + +@Slf4j +@Component +public class JpaTbResourceInfoDao extends JpaAbstractSearchTextDao implements TbResourceInfoDao { + + @Autowired + private TbResourceInfoRepository resourceInfoRepository; + + @Override + protected Class getEntityClass() { + return TbResourceInfoEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return resourceInfoRepository; + } + + @Override + public PageData findAllTenantResourcesByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData(resourceInfoRepository + .findAllTenantResourcesByTenantId( + tenantId, + TenantId.NULL_UUID, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findTenantResourcesByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData(resourceInfoRepository + .findTenantResourcesByTenantId( + tenantId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/ResourceDaoImpl.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/ResourceDaoImpl.java deleted file mode 100644 index e0cddcc555..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/ResourceDaoImpl.java +++ /dev/null @@ -1,84 +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.dao.sql.resource; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; -import org.thingsboard.server.common.data.Resource; -import org.thingsboard.server.common.data.ResourceType; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.sql.ResourceCompositeKey; -import org.thingsboard.server.dao.model.sql.ResourceEntity; -import org.thingsboard.server.dao.resource.ResourceDao; - -import java.util.List; - -@Slf4j -@Component -public class ResourceDaoImpl implements ResourceDao { - - private final ResourceRepository resourceRepository; - - public ResourceDaoImpl(ResourceRepository resourceRepository) { - this.resourceRepository = resourceRepository; - } - - @Override - @Transactional - public Resource saveResource(Resource resource) { - return DaoUtil.getData(resourceRepository.save(new ResourceEntity(resource))); - } - - @Override - public Resource getResource(TenantId tenantId, ResourceType resourceType, String resourceId) { - ResourceCompositeKey key = new ResourceCompositeKey(); - key.setTenantId(tenantId.getId()); - key.setResourceType(resourceType.name()); - key.setResourceId(resourceId); - - return DaoUtil.getData(resourceRepository.findById(key)); - } - - @Override - @Transactional - public void deleteResource(TenantId tenantId, ResourceType resourceType, String resourceId) { - ResourceCompositeKey key = new ResourceCompositeKey(); - key.setTenantId(tenantId.getId()); - key.setResourceType(resourceType.name()); - key.setResourceId(resourceId); - - resourceRepository.deleteById(key); - } - - @Override - public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { - return DaoUtil.toPageData(resourceRepository.findAllByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); - } - - @Override - public List findAllByTenantIdResourceType(TenantId tenantId, ResourceType resourceType) { - return DaoUtil.convertDataList(resourceRepository.findAllByTenantIdAndResourceType(tenantId.getId(), resourceType.name())); - } - - @Override - public void removeAllByTenantId(TenantId tenantId) { - resourceRepository.removeAllByTenantId(tenantId.getId()); - } -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/ResourceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/ResourceRepository.java deleted file mode 100644 index c33c9786bd..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/ResourceRepository.java +++ /dev/null @@ -1,35 +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.dao.sql.resource; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.repository.CrudRepository; -import org.thingsboard.server.dao.model.sql.ResourceCompositeKey; -import org.thingsboard.server.dao.model.sql.ResourceEntity; - -import java.util.List; -import java.util.UUID; - -public interface ResourceRepository extends CrudRepository { - - Page findAllByTenantId(UUID tenantId, Pageable pageable); - - - List findAllByTenantIdAndResourceType(UUID tenantId, String resourceType); - - void removeAllByTenantId(UUID tenantId); -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java new file mode 100644 index 0000000000..db1c5273c8 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java @@ -0,0 +1,49 @@ +/** + * 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.dao.sql.resource; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.dao.model.sql.TbResourceInfoEntity; + +import java.util.UUID; + +public interface TbResourceInfoRepository extends CrudRepository { + + @Query("SELECT tr FROM TbResourceInfoEntity tr WHERE " + + "LOWER(tr.searchText) LIKE LOWER(CONCAT(:searchText, '%'))" + + "AND (tr.tenantId = :tenantId " + + "OR (tr.tenantId = :systemAdminId " + + "AND NOT EXISTS " + + "(SELECT sr FROM TbResourceEntity sr " + + "WHERE sr.tenantId = :tenantId " + + "AND tr.resourceType = sr.resourceType " + + "AND tr.resourceKey = sr.resourceKey)))") + Page findAllTenantResourcesByTenantId(@Param("tenantId") UUID tenantId, + @Param("systemAdminId") UUID sysadminId, + @Param("searchText") String searchText, + Pageable pageable); + + @Query("SELECT ri FROM TbResourceInfoEntity ri WHERE " + + "ri.tenantId = :tenantId " + + "AND LOWER(ri.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findTenantResourcesByTenantId(@Param("tenantId") UUID tenantId, + @Param("searchText") String searchText, + Pageable pageable); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java new file mode 100644 index 0000000000..488192eb13 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java @@ -0,0 +1,82 @@ +/** + * 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.dao.sql.resource; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.dao.model.sql.TbResourceEntity; + +import java.util.List; +import java.util.UUID; + +public interface TbResourceRepository extends CrudRepository { + + TbResourceEntity findByTenantIdAndResourceTypeAndResourceKey(UUID tenantId, String resourceType, String resourceKey); + + Page findAllByTenantId(UUID tenantId, Pageable pageable); + + @Query("SELECT tr FROM TbResourceEntity tr " + + "WHERE tr.resourceType = :resourceType " + + "AND LOWER(tr.searchText) LIKE LOWER(CONCAT('%', :searchText, '%')) " + + "AND (tr.tenantId = :tenantId " + + "OR (tr.tenantId = :systemAdminId " + + "AND NOT EXISTS " + + "(SELECT sr FROM TbResourceEntity sr " + + "WHERE sr.tenantId = :tenantId " + + "AND sr.resourceType = :resourceType " + + "AND tr.resourceKey = sr.resourceKey)))") + Page findResourcesPage( + @Param("tenantId") UUID tenantId, + @Param("systemAdminId") UUID sysAdminId, + @Param("resourceType") String resourceType, + @Param("searchText") String search, + Pageable pageable); + + void removeAllByTenantId(UUID tenantId); + + @Query("SELECT tr FROM TbResourceEntity tr " + + "WHERE tr.resourceType = :resourceType " + + "AND LOWER(tr.searchText) LIKE LOWER(CONCAT('%', :searchText, '%')) " + + "AND (tr.tenantId = :tenantId " + + "OR (tr.tenantId = :systemAdminId " + + "AND NOT EXISTS " + + "(SELECT sr FROM TbResourceEntity sr " + + "WHERE sr.tenantId = :tenantId " + + "AND sr.resourceType = :resourceType " + + "AND tr.resourceKey = sr.resourceKey)))") + List findResources(@Param("tenantId") UUID tenantId, + @Param("systemAdminId") UUID sysAdminId, + @Param("resourceType") String resourceType, + @Param("searchText") String search); + + @Query("SELECT tr FROM TbResourceEntity tr " + + "WHERE tr.resourceType = :resourceType " + + "AND tr.resourceKey in (:resourceIds) " + + "AND (tr.tenantId = :tenantId " + + "OR (tr.tenantId = :systemAdminId " + + "AND NOT EXISTS " + + "(SELECT sr FROM TbResourceEntity sr " + + "WHERE sr.tenantId = :tenantId " + + "AND sr.resourceType = :resourceType " + + "AND tr.resourceKey = sr.resourceKey)))") + List findResourcesByIds(@Param("tenantId") UUID tenantId, + @Param("systemAdminId") UUID sysAdminId, + @Param("resourceType") String resourceType, + @Param("resourceIds") String[] objectIds); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index b514b43cf3..9a08c65fef 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -35,7 +35,7 @@ import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.resource.ResourceService; +import org.thingsboard.server.dao.resource.TbResourceService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -90,7 +90,7 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe private RuleChainService ruleChainService; @Autowired - private ResourceService resourceService; + private TbResourceService resourceService; @Override public Tenant findTenantById(TenantId tenantId) { diff --git a/dao/src/main/resources/sql/schema-entities-hsql.sql b/dao/src/main/resources/sql/schema-entities-hsql.sql index 96d2a8ff13..749f3f9aa3 100644 --- a/dao/src/main/resources/sql/schema-entities-hsql.sql +++ b/dao/src/main/resources/sql/schema-entities-hsql.sql @@ -426,11 +426,16 @@ CREATE TABLE IF NOT EXISTS api_usage_state ( ); CREATE TABLE IF NOT EXISTS resource ( + id uuid NOT NULL CONSTRAINT resource_pkey PRIMARY KEY, + created_time bigint NOT NULL, tenant_id uuid NOT NULL, + title varchar(255) NOT NULL, resource_type varchar(32) NOT NULL, - resource_id varchar(255) NOT NULL, - resource_value varchar, - CONSTRAINT resource_unq_key UNIQUE (tenant_id, resource_type, resource_id) + resource_key varchar(255) NOT NULL, + search_text varchar(255), + file_name varchar(255) NOT NULL, + data varchar, + CONSTRAINT resource_unq_key UNIQUE (tenant_id, resource_type, resource_key) ); CREATE TABLE IF NOT EXISTS edge ( diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 3d37f63f92..7d65e4f8dc 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -453,11 +453,16 @@ CREATE TABLE IF NOT EXISTS api_usage_state ( ); CREATE TABLE IF NOT EXISTS resource ( + id uuid NOT NULL CONSTRAINT resource_pkey PRIMARY KEY, + created_time bigint NOT NULL, tenant_id uuid NOT NULL, + title varchar(255) NOT NULL, resource_type varchar(32) NOT NULL, - resource_id varchar(255) NOT NULL, - resource_value varchar, - CONSTRAINT resource_unq_key UNIQUE (tenant_id, resource_type, resource_id) + resource_key varchar(255) NOT NULL, + search_text varchar(255), + file_name varchar(255) NOT NULL, + data varchar, + CONSTRAINT resource_unq_key UNIQUE (tenant_id, resource_type, resource_key) ); CREATE TABLE IF NOT EXISTS edge ( diff --git a/dao/src/main/resources/xss-policy.xml b/dao/src/main/resources/xss-policy.xml new file mode 100644 index 0000000000..6ea6660d2b --- /dev/null +++ b/dao/src/main/resources/xss-policy.xml @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + g + grin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java index c5997af67b..cfd232b330 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java @@ -54,6 +54,7 @@ import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.resource.TbResourceService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.tenant.TenantProfileService; @@ -150,6 +151,9 @@ public abstract class AbstractServiceTest { @Autowired protected DeviceProfileService deviceProfileService; + @Autowired + protected TbResourceService resourceService; + class IdComparator implements Comparator { @Override public int compare(D o1, D o2) { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTbResourceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTbResourceServiceTest.java new file mode 100644 index 0000000000..21842af176 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTbResourceServiceTest.java @@ -0,0 +1,350 @@ +/** + * 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.dao.service; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.exception.DataValidationException; + +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.List; + +public abstract class BaseTbResourceServiceTest extends AbstractServiceTest { + + private static final String LWM2M_TEST_MODEL = "\n" + + "\n" + + "My first resource\n" + + "\n" + + "0\n" + + "\n" + + "1.0\n" + + "Multiple\n" + + "Mandatory\n" + + "\n" + + "\n" + + "LWM2M\n" + + "\n" + + "Single\n" + + "Mandatory\n" + + "String\n" + + "0..255\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + ""; + + private static final String DEFAULT_FILE_NAME = "test.jks"; + + private IdComparator idComparator = new IdComparator<>(); + + private TenantId tenantId; + + @Before + public void before() { + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + Tenant savedTenant = tenantService.saveTenant(tenant); + Assert.assertNotNull(savedTenant); + tenantId = savedTenant.getId(); + } + + @After + public void after() { + tenantService.deleteTenant(tenantId); + } + + @Test + public void testSaveTbResource() throws Exception { + TbResource resource = new TbResource(); + resource.setTenantId(tenantId); + resource.setResourceType(ResourceType.JKS); + resource.setTitle("My first resource"); + resource.setFileName(DEFAULT_FILE_NAME); + resource.setData("Test Data"); + + TbResource savedResource = resourceService.saveResource(resource); + + Assert.assertNotNull(savedResource); + Assert.assertNotNull(savedResource.getId()); + Assert.assertTrue(savedResource.getCreatedTime() > 0); + Assert.assertEquals(resource.getTenantId(), savedResource.getTenantId()); + Assert.assertEquals(resource.getTitle(), savedResource.getTitle()); + Assert.assertEquals(resource.getResourceKey(), savedResource.getResourceKey()); + Assert.assertEquals(resource.getData(), savedResource.getData()); + + savedResource.setTitle("My new resource"); + + resourceService.saveResource(savedResource); + TbResource foundResource = resourceService.findResourceById(tenantId, savedResource.getId()); + Assert.assertEquals(foundResource.getTitle(), savedResource.getTitle()); + + resourceService.deleteResource(tenantId, savedResource.getId()); + } + + @Test + public void testSaveLwm2mTbResource() throws Exception { + TbResource resource = new TbResource(); + resource.setTenantId(tenantId); + resource.setResourceType(ResourceType.LWM2M_MODEL); + resource.setFileName("test_model.xml"); + resource.setData(Base64.getEncoder().encodeToString(LWM2M_TEST_MODEL.getBytes())); + + TbResource savedResource = resourceService.saveResource(resource); + + Assert.assertNotNull(savedResource); + Assert.assertNotNull(savedResource.getId()); + Assert.assertTrue(savedResource.getCreatedTime() > 0); + Assert.assertEquals(resource.getTenantId(), savedResource.getTenantId()); + Assert.assertEquals("My first resource id=0 v1.0", savedResource.getTitle()); + Assert.assertEquals("0_1.0", savedResource.getResourceKey()); + Assert.assertEquals(resource.getData(), savedResource.getData()); + + resourceService.deleteResource(tenantId, savedResource.getId()); + } + + @Test + public void testSaveTbResourceWithEmptyTenant() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JKS); + resource.setTitle("My resource"); + resource.setFileName(DEFAULT_FILE_NAME); + resource.setData("Test Data"); + TbResource savedResource = resourceService.saveResource(resource); + + Assert.assertEquals(TenantId.SYS_TENANT_ID, savedResource.getTenantId()); + + resourceService.deleteResource(tenantId, savedResource.getId()); + } + + @Test(expected = DataValidationException.class) + public void testSaveTbResourceWithExistsFileName() throws Exception { + TbResource resource = new TbResource(); + resource.setTenantId(tenantId); + resource.setResourceType(ResourceType.JKS); + resource.setTitle("My resource"); + resource.setFileName(DEFAULT_FILE_NAME); + resource.setData("Test Data"); + + TbResource savedResource = resourceService.saveResource(resource); + + TbResource resource2 = new TbResource(); + resource.setTenantId(tenantId); + resource.setResourceType(ResourceType.JKS); + resource.setTitle("My resource"); + resource.setFileName(DEFAULT_FILE_NAME); + resource.setData("Test Data"); + + try { + resourceService.saveResource(resource2); + } finally { + resourceService.deleteResource(tenantId, savedResource.getId()); + } + } + + @Test(expected = DataValidationException.class) + public void testSaveTbResourceWithEmptyTitle() throws Exception { + TbResource resource = new TbResource(); + resource.setTenantId(tenantId); + resource.setResourceType(ResourceType.JKS); + resource.setFileName(DEFAULT_FILE_NAME); + resource.setData("Test Data"); + resourceService.saveResource(resource); + } + + @Test(expected = DataValidationException.class) + public void testSaveTbResourceWithInvalidTenant() throws Exception { + TbResource resource = new TbResource(); + resource.setTenantId(new TenantId(Uuids.timeBased())); + resource.setResourceType(ResourceType.JKS); + resource.setTitle("My resource"); + resource.setFileName(DEFAULT_FILE_NAME); + resource.setData("Test Data"); + resourceService.saveResource(resource); + } + + @Test + public void testFindResourceById() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JKS); + resource.setTitle("My resource"); + resource.setFileName(DEFAULT_FILE_NAME); + resource.setData("Test Data"); + TbResource savedResource = resourceService.saveResource(resource); + + TbResource foundResource = resourceService.findResourceById(tenantId, savedResource.getId()); + Assert.assertNotNull(foundResource); + Assert.assertEquals(savedResource, foundResource); + resourceService.deleteResource(tenantId, savedResource.getId()); + } + + @Test + public void testFindResourceByTenantIdAndResourceTypeAndResourceKey() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JKS); + resource.setTenantId(tenantId); + resource.setTitle("My resource"); + resource.setFileName(DEFAULT_FILE_NAME); + resource.setData("Test Data"); + TbResource savedResource = resourceService.saveResource(resource); + + TbResource foundResource = resourceService.getResource(tenantId, savedResource.getResourceType(), savedResource.getResourceKey()); + Assert.assertNotNull(foundResource); + Assert.assertEquals(savedResource, foundResource); + resourceService.deleteResource(tenantId, savedResource.getId()); + } + + @Test + public void testDeleteResource() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JKS); + resource.setTitle("My resource"); + resource.setFileName(DEFAULT_FILE_NAME); + resource.setData("Test Data"); + TbResource savedResource = resourceService.saveResource(resource); + + TbResource foundResource = resourceService.findResourceById(tenantId, savedResource.getId()); + Assert.assertNotNull(foundResource); + resourceService.deleteResource(tenantId, savedResource.getId()); + foundResource = resourceService.findResourceById(tenantId, savedResource.getId()); + Assert.assertNull(foundResource); + } + + @Test + public void testFindTenantResourcesByTenantId() throws Exception { + Tenant tenant = new Tenant(); + tenant.setTitle("Test tenant"); + tenant = tenantService.saveTenant(tenant); + + TenantId tenantId = tenant.getId(); + + List resources = new ArrayList<>(); + for (int i = 0; i < 165; i++) { + TbResource resource = new TbResource(); + resource.setTenantId(tenantId); + resource.setTitle("Resource" + i); + resource.setResourceType(ResourceType.JKS); + resource.setFileName(i + DEFAULT_FILE_NAME); + resource.setData("Test Data"); + resources.add(new TbResourceInfo(resourceService.saveResource(resource))); + } + + List loadedResources = new ArrayList<>(); + PageLink pageLink = new PageLink(16); + PageData pageData; + do { + pageData = resourceService.findTenantResourcesByTenantId(tenantId, pageLink); + loadedResources.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(resources, idComparator); + Collections.sort(loadedResources, idComparator); + + Assert.assertEquals(resources, loadedResources); + + resourceService.deleteResourcesByTenantId(tenantId); + + pageLink = new PageLink(31); + pageData = resourceService.findTenantResourcesByTenantId(tenantId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertTrue(pageData.getData().isEmpty()); + + tenantService.deleteTenant(tenantId); + } + + @Test + public void testFindAllTenantResourcesByTenantId() throws Exception { + Tenant tenant = new Tenant(); + tenant.setTitle("Test tenant"); + tenant = tenantService.saveTenant(tenant); + + TenantId tenantId = tenant.getId(); + + List resources = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + TbResource resource = new TbResource(); + resource.setTenantId(TenantId.SYS_TENANT_ID); + resource.setTitle("System Resource" + i); + resource.setResourceType(ResourceType.JKS); + resource.setFileName(i + DEFAULT_FILE_NAME); + resource.setData("Test Data"); + TbResourceInfo tbResourceInfo = new TbResourceInfo(resourceService.saveResource(resource)); + if (i >= 50) { + resources.add(tbResourceInfo); + } + } + + for (int i = 0; i < 50; i++) { + TbResource resource = new TbResource(); + resource.setTenantId(tenantId); + resource.setTitle("Tenant Resource" + i); + resource.setResourceType(ResourceType.JKS); + resource.setFileName(i + DEFAULT_FILE_NAME); + resource.setData("Test Data"); + resources.add(new TbResourceInfo(resourceService.saveResource(resource))); + } + + List loadedResources = new ArrayList<>(); + PageLink pageLink = new PageLink(10); + PageData pageData; + do { + pageData = resourceService.findAllTenantResourcesByTenantId(tenantId, pageLink); + loadedResources.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(resources, idComparator); + Collections.sort(loadedResources, idComparator); + + Assert.assertEquals(resources, loadedResources); + + resourceService.deleteResourcesByTenantId(tenantId); + + pageLink = new PageLink(100); + pageData = resourceService.findAllTenantResourcesByTenantId(tenantId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(pageData.getData().size(), 100); + + resourceService.deleteResourcesByTenantId(TenantId.SYS_TENANT_ID); + + pageLink = new PageLink(100); + pageData = resourceService.findAllTenantResourcesByTenantId(TenantId.SYS_TENANT_ID, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertTrue(pageData.getData().isEmpty()); + + tenantService.deleteTenant(tenantId); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/NoXssValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/NoXssValidatorTest.java new file mode 100644 index 0000000000..8463e722cb --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/NoXssValidatorTest.java @@ -0,0 +1,52 @@ +/** + * 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.dao.service; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import javax.validation.ConstraintValidatorContext; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.Mockito.mock; + +public class NoXssValidatorTest { + private static NoXssValidator validator; + + @BeforeAll + public static void beforeAll() { + validator = new NoXssValidator(); + validator.initialize(null); + } + + @ParameterizedTest + @ValueSource(strings = { + "aboba666", + "909090909", + "qwertyyyy", + "bambam", + "

Link!!!

1221", + "

Please log in to proceed

Username:

Password:



", + " ", + "123 bebe", + }) + public void testIsNotValid(String stringWithXss) { + boolean isValid = validator.isValid(stringWithXss, mock(ConstraintValidatorContext.class)); + assertFalse(isValid); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/TbResourceServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/TbResourceServiceSqlTest.java new file mode 100644 index 0000000000..2af4881c84 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/TbResourceServiceSqlTest.java @@ -0,0 +1,23 @@ +/** + * 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.dao.service.sql; + +import org.thingsboard.server.dao.service.BaseTbResourceServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class TbResourceServiceSqlTest extends BaseTbResourceServiceTest { +} diff --git a/dao/src/test/resources/xss-policy.xml b/dao/src/test/resources/xss-policy.xml new file mode 100644 index 0000000000..6ea6660d2b --- /dev/null +++ b/dao/src/test/resources/xss-policy.xml @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + g + grin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index b71a5c097b..1f2047b052 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -230,7 +230,7 @@ services: haproxy: restart: always container_name: "${LOAD_BALANCER_NAME}" - image: xalauc/haproxy-certbot:1.7.9 + image: thingsboard/haproxy-certbot:1.3.0 volumes: - ./haproxy/config:/config - ./haproxy/letsencrypt:/etc/letsencrypt diff --git a/docker/haproxy/config/haproxy.cfg b/docker/haproxy/config/haproxy.cfg index 5ff76cfdcd..2a457a0696 100644 --- a/docker/haproxy/config/haproxy.cfg +++ b/docker/haproxy/config/haproxy.cfg @@ -54,7 +54,7 @@ frontend http-in option forwardfor - reqadd X-Forwarded-Proto:\ http + http-request add-header "X-Forwarded-Proto" "http" acl transport_http_acl path_beg /api/v1/ acl letsencrypt_http_acl path_beg /.well-known/acme-challenge/ @@ -73,7 +73,7 @@ frontend https_in option forwardfor - reqadd X-Forwarded-Proto:\ https + http-request add-header "X-Forwarded-Proto" "https" acl transport_http_acl path_beg /api/v1/ acl tb_api_acl path_beg /api/ /swagger /webjars /v2/ /static/rulenode/ /oauth2/ /login/oauth2/ /static/widgets/ diff --git a/pom.xml b/pom.xml index 970d0d08c6..c26964e8e9 100755 --- a/pom.xml +++ b/pom.xml @@ -47,6 +47,7 @@ 0.7.0 2.2.0 4.12 + 5.7.1 1.7.7 1.2.3 3.3.3 @@ -116,6 +117,10 @@ 1.0.2TB 3.4.0 7.54.2 + 6.0.13.Final + 3.0.0 + 2.0.1.Final + 1.6.2 @@ -933,6 +938,11 @@ stats ${project.version} + + org.thingsboard.common + coap-server + ${project.version} + org.thingsboard tools @@ -1241,6 +1251,11 @@ test-jar test + + org.eclipse.californium + scandium + ${californium.version} + com.google.code.gson gson @@ -1345,6 +1360,12 @@ ${junit.version} test + + org.junit.jupiter + junit-jupiter-params + ${jupiter.version} + test + org.dbunit dbunit @@ -1542,6 +1563,36 @@ + + org.hibernate.validator + hibernate-validator + ${hibernate-validator.version} + + + org.glassfish + javax.el + ${javax.el.version} + + + javax.validation + validation-api + ${javax.validation-api.version} + + + org.owasp.antisamy + antisamy + ${antisamy.version} + + + org.slf4j + * + + + com.github.spotbugs + spotbugs-annotations + + + diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java index ab0d9df7c2..928a403fc4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java @@ -33,10 +33,7 @@ import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpe import org.thingsboard.server.common.data.device.profile.SpecificTimeSchedule; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.ComplexFilterPredicate; -import org.thingsboard.server.common.data.query.EntityKey; -import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.FilterPredicateValue; -import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.KeyFilterPredicate; import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.query.StringFilterPredicate; @@ -275,7 +272,7 @@ class AlarmRuleState { if (value == null) { return false; } - eval = eval && eval(data, value, filter.getPredicate()); + eval = eval && eval(data, value, filter.getPredicate(), filter); } return eval; } @@ -300,33 +297,33 @@ class AlarmRuleState { return value; } - private boolean eval(DataSnapshot data, EntityKeyValue value, KeyFilterPredicate predicate) { + private boolean eval(DataSnapshot data, EntityKeyValue value, KeyFilterPredicate predicate, AlarmConditionFilter filter) { switch (predicate.getType()) { case STRING: - return evalStrPredicate(data, value, (StringFilterPredicate) predicate); + return evalStrPredicate(data, value, (StringFilterPredicate) predicate, filter); case NUMERIC: - return evalNumPredicate(data, value, (NumericFilterPredicate) predicate); + return evalNumPredicate(data, value, (NumericFilterPredicate) predicate, filter); case BOOLEAN: - return evalBoolPredicate(data, value, (BooleanFilterPredicate) predicate); + return evalBoolPredicate(data, value, (BooleanFilterPredicate) predicate, filter); case COMPLEX: - return evalComplexPredicate(data, value, (ComplexFilterPredicate) predicate); + return evalComplexPredicate(data, value, (ComplexFilterPredicate) predicate, filter); default: return false; } } - private boolean evalComplexPredicate(DataSnapshot data, EntityKeyValue ekv, ComplexFilterPredicate predicate) { + private boolean evalComplexPredicate(DataSnapshot data, EntityKeyValue ekv, ComplexFilterPredicate predicate, AlarmConditionFilter filter) { switch (predicate.getOperation()) { case OR: for (KeyFilterPredicate kfp : predicate.getPredicates()) { - if (eval(data, ekv, kfp)) { + if (eval(data, ekv, kfp, filter)) { return true; } } return false; case AND: for (KeyFilterPredicate kfp : predicate.getPredicates()) { - if (!eval(data, ekv, kfp)) { + if (!eval(data, ekv, kfp, filter)) { return false; } } @@ -336,12 +333,15 @@ class AlarmRuleState { } } - private boolean evalBoolPredicate(DataSnapshot data, EntityKeyValue ekv, BooleanFilterPredicate predicate) { + private boolean evalBoolPredicate(DataSnapshot data, EntityKeyValue ekv, BooleanFilterPredicate predicate, AlarmConditionFilter filter) { Boolean val = getBoolValue(ekv); if (val == null) { return false; } - Boolean predicateValue = getPredicateValue(data, predicate.getValue(), AlarmRuleState::getBoolValue); + Boolean predicateValue = getPredicateValue(data, predicate.getValue(), filter, AlarmRuleState::getBoolValue); + if (predicateValue == null) { + return false; + } switch (predicate.getOperation()) { case EQUAL: return val.equals(predicateValue); @@ -352,12 +352,15 @@ class AlarmRuleState { } } - private boolean evalNumPredicate(DataSnapshot data, EntityKeyValue ekv, NumericFilterPredicate predicate) { + private boolean evalNumPredicate(DataSnapshot data, EntityKeyValue ekv, NumericFilterPredicate predicate, AlarmConditionFilter filter) { Double val = getDblValue(ekv); if (val == null) { return false; } - Double predicateValue = getPredicateValue(data, predicate.getValue(), AlarmRuleState::getDblValue); + Double predicateValue = getPredicateValue(data, predicate.getValue(), filter, AlarmRuleState::getDblValue); + if (predicateValue == null) { + return false; + } switch (predicate.getOperation()) { case NOT_EQUAL: return !val.equals(predicateValue); @@ -376,12 +379,15 @@ class AlarmRuleState { } } - private boolean evalStrPredicate(DataSnapshot data, EntityKeyValue ekv, StringFilterPredicate predicate) { + private boolean evalStrPredicate(DataSnapshot data, EntityKeyValue ekv, StringFilterPredicate predicate, AlarmConditionFilter filter) { String val = getStrValue(ekv); if (val == null) { return false; } - String predicateValue = getPredicateValue(data, predicate.getValue(), AlarmRuleState::getStrValue); + String predicateValue = getPredicateValue(data, predicate.getValue(), filter, AlarmRuleState::getStrValue); + if (predicateValue == null) { + return false; + } if (predicate.isIgnoreCase()) { val = val.toLowerCase(); predicateValue = predicateValue.toLowerCase(); @@ -404,7 +410,7 @@ class AlarmRuleState { } } - private T getPredicateValue(DataSnapshot data, FilterPredicateValue value, Function transformFunction) { + private T getPredicateValue(DataSnapshot data, FilterPredicateValue value, AlarmConditionFilter filter, Function transformFunction) { EntityKeyValue ekv = getDynamicPredicateValue(data, value); if (ekv != null) { T result = transformFunction.apply(ekv); @@ -412,7 +418,11 @@ class AlarmRuleState { return result; } } - return value.getDefaultValue(); + if (filter.getKey().getType() != AlarmConditionKeyType.CONSTANT) { + return value.getDefaultValue(); + } else { + return null; + } } private EntityKeyValue getDynamicPredicateValue(DataSnapshot data, FilterPredicateValue value) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java index 201b998129..ceee362fc8 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java @@ -21,8 +21,8 @@ import com.google.common.util.concurrent.ListenableFuture; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.rule.engine.action.TbAlarmResult; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.profile.state.PersistedAlarmRuleState; diff --git a/transport/coap/src/main/java/org/thingsboard/server/coap/ThingsboardCoapTransportApplication.java b/transport/coap/src/main/java/org/thingsboard/server/coap/ThingsboardCoapTransportApplication.java index 7afafcfb73..010a774e0e 100644 --- a/transport/coap/src/main/java/org/thingsboard/server/coap/ThingsboardCoapTransportApplication.java +++ b/transport/coap/src/main/java/org/thingsboard/server/coap/ThingsboardCoapTransportApplication.java @@ -26,7 +26,7 @@ import java.util.Arrays; @SpringBootConfiguration @EnableAsync @EnableScheduling -@ComponentScan({"org.thingsboard.server.coap", "org.thingsboard.server.common", "org.thingsboard.server.transport.coap", "org.thingsboard.server.queue"}) +@ComponentScan({"org.thingsboard.server.coap", "org.thingsboard.server.common", "org.thingsboard.server.coapserver", "org.thingsboard.server.transport.coap", "org.thingsboard.server.queue"}) public class ThingsboardCoapTransportApplication { private static final String SPRING_CONFIG_NAME_KEY = "--spring.config.name"; diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index a9fe673b28..4d5bbf2c6f 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -46,6 +46,28 @@ transport: bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}" bind_port: "${COAP_BIND_PORT:5683}" timeout: "${COAP_TIMEOUT:10000}" + dtls: + # Enable/disable DTLS 1.2 support + enabled: "${COAP_DTLS_ENABLED:false}" + # CoAP DTLS bind address + bind_address: "${COAP_DTLS_BIND_ADDRESS:0.0.0.0}" + # CoAP DTLS bind port + bind_port: "${COAP_DTLS_BIND_PORT:5684}" + # Secure mode. Allowed values: NO_AUTH, X509 + mode: "${COAP_DTLS_SECURE_MODE:NO_AUTH}" + # Path to the key store that holds the certificate + key_store: "${COAP_DTLS_KEY_STORE:coapserver.jks}" + # Password used to access the key store + key_store_password: "${COAP_DTLS_KEY_STORE_PASSWORD:server_ks_password}" + # Password used to access the key + key_password: "${COAP_DTLS_KEY_PASSWORD:server_key_password}" + # Key alias + key_alias: "${COAP_DTLS_KEY_ALIAS:serveralias}" + # Skip certificate validity check for client certificates. + skip_validity_check_for_client_cert: "${COAP_DTLS_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" + x509: + dtls_session_inactivity_timeout: "${TB_COAP_X509_DTLS_SESSION_INACTIVITY_TIMEOUT:86400000}" + dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}" sessions: inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}" report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}" diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 8617868771..ec63dcae75 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -127,12 +127,9 @@ transport: # send a Confirmable message to the time when an acknowledgement is no longer expected. # DEFAULT_TIMEOUT = 2 * 60 * 1000l; 2 min in ms timeout: "${LWM2M_TIMEOUT:120000}" - # model_path_file: "${LWM2M_MODEL_PATH_FILE:./common/transport/lwm2m/src/main/resources/models/}" - model_path_file: "${LWM2M_MODEL_PATH_FILE:}" recommended_ciphers: "${LWM2M_RECOMMENDED_CIPHERS:false}" recommended_supported_groups: "${LWM2M_RECOMMENDED_SUPPORTED_GROUPS:true}" - request_pool_size: "${LWM2M_REQUEST_POOL_SIZE:100}" - request_error_pool_size: "${LWM2M_REQUEST_ERROR_POOL_SIZE:10}" + response_pool_size: "${LWM2M_RESPONSE_POOL_SIZE:100}" registered_pool_size: "${LWM2M_REGISTERED_POOL_SIZE:10}" update_registered_pool_size: "${LWM2M_UPDATE_REGISTERED_POOL_SIZE:10}" un_registered_pool_size: "${LWM2M_UN_REGISTERED_POOL_SIZE:10}" @@ -141,8 +138,7 @@ transport: # To get helps about files format and how to generate it, see: https://github.com/eclipse/leshan/wiki/Credential-files-format # Create new X509 Certificates: common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_credentials.sh key_store_type: "${LWM2M_KEYSTORE_TYPE:JKS}" - # key_store_type: "${LWM2M_KEYSTORE_TYPE:PKCS12}" - # key_store_path_file: "${KEY_STORE_PATH_FILE:/usr/share/thingsboard/conf/credentials/serverKeyStore.jks}" + # key_store_path_file: "${KEY_STORE_PATH_FILE:/transport/lwm2m/src/main/data/credentials/serverKeyStore.jks}" key_store_path_file: "${KEY_STORE_PATH_FILE:}" key_store_password: "${LWM2M_KEYSTORE_PASSWORD_SERVER:server_ks_password}" root_alias: "${LWM2M_SERVER_ROOT_CA:rootca}" @@ -161,24 +157,26 @@ transport: # - Elliptic Curve parameters : [secp256r1 [NIST P-256, X9.62 prime256v1] (1.2.840.10045.3.1.7)] public_x: "${LWM2M_SERVER_PUBLIC_X:05064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f358}" public_y: "${LWM2M_SERVER_PUBLIC_Y:5eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b}" - private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED:308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420dc774b309e547ceb48fee547e104ce201a9c48c449dc5414cd04e7f5cf05f67ba00a06082a8648ce3d030107a1440342000405064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f3585eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b}" # Only Certificate_x509: + private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED:308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420dc774b309e547ceb48fee547e104ce201a9c48c449dc5414cd04e7f5cf05f67ba00a06082a8648ce3d030107a1440342000405064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f3585eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b}" + # Only Certificate_x509: alias: "${LWM2M_KEYSTORE_ALIAS_SERVER:server}" bootstrap: - enable: "${LWM2M_BOOTSTRAP_ENABLED:true}" - id: "${LWM2M_SERVER_ID:111}" + enable: "${LWM2M_ENABLED_BS:true}" + id: "${LWM2M_SERVER_ID_BS:111}" bind_address: "${LWM2M_BIND_ADDRESS_BS:0.0.0.0}" bind_port_no_sec: "${LWM2M_BIND_PORT_NO_SEC_BS:5687}" secure: bind_address_security: "${LWM2M_BIND_ADDRESS_BS:0.0.0.0}" - bind_port_security: "${LWM2M_BIND_PORT_SEC_BS:5688}" + bind_port_security: "${LWM2M_BIND_PORT_SECURITY_BS:5688}" # Only for RPK: Public & Private Key. If the keystore file is missing or not working # - Elliptic Curve parameters : [secp256r1 [NIST P-256, X9.62 prime256v1] (1.2.840.10045.3.1.7)] # - Public Key (Hex): [3059301306072a8648ce3d020106082a8648ce3d030107034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34] # - Private Key (Hex): [308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104205ecafd90caa7be45c42e1f3f32571632b8409e6e6249d7124f4ba56fab3c8083a00a06082a8648ce3d030107a144034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34], public_x: "${LWM2M_SERVER_PUBLIC_X_BS:5017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f91}" public_y: "${LWM2M_SERVER_PUBLIC_Y_BS:3fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34}" - private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED_BS:308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104205ecafd90caa7be45c42e1f3f32571632b8409e6e6249d7124f4ba56fab3c8083a00a06082a8648ce3d030107a144034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34}" # Only Certificate_x509: - alias: "${LWM2M_KEYSTORE_ALIAS_BOOTSTRAP:bootstrap}" + private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED_BS:308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104205ecafd90caa7be45c42e1f3f32571632b8409e6e6249d7124f4ba56fab3c8083a00a06082a8648ce3d030107a144034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34}" + # Only Certificate_x509: + alias: "${LWM2M_KEYSTORE_ALIAS_BS:bootstrap}" # Use redis for Security and Registration stores redis.enabled: "${LWM2M_REDIS_ENABLED:false}" diff --git a/ui-ngx/src/app/core/http/device-profile.service.ts b/ui-ngx/src/app/core/http/device-profile.service.ts index e0a0bf5174..f7e82f3a41 100644 --- a/ui-ngx/src/app/core/http/device-profile.service.ts +++ b/ui-ngx/src/app/core/http/device-profile.service.ts @@ -14,16 +14,16 @@ /// limitations under the License. /// -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { PageLink } from '@shared/models/page/page-link'; -import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; -import { Observable } from 'rxjs'; -import { PageData } from '@shared/models/page/page-data'; -import { DeviceProfile, DeviceProfileInfo, DeviceTransportType } from '@shared/models/device.models'; -import { isDefinedAndNotNull, isEmptyStr } from '@core/utils'; -import { ObjectLwM2M, ServerSecurityConfig } from '@home/components/profile/device/lwm2m/profile-config.models'; -import { SortOrder } from '@shared/models/page/sort-order'; +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {PageLink} from '@shared/models/page/page-link'; +import {defaultHttpOptionsFromConfig, RequestConfig} from './http-utils'; +import {Observable} from 'rxjs'; +import {PageData} from '@shared/models/page/page-data'; +import {DeviceProfile, DeviceProfileInfo, DeviceTransportType} from '@shared/models/device.models'; +import {isDefinedAndNotNull, isEmptyStr} from '@core/utils'; +import {ObjectLwM2M, ServerSecurityConfig} from '@home/components/profile/device/lwm2m/profile-config.models'; +import {SortOrder} from '@shared/models/page/sort-order'; @Injectable({ providedIn: 'root' @@ -43,9 +43,9 @@ export class DeviceProfileService { return this.http.get(`/api/deviceProfile/${deviceProfileId}`, defaultHttpOptionsFromConfig(config)); } - public getLwm2mObjects(sortOrder: SortOrder, objectIds?: number[], searchText?: string, config?: RequestConfig): + public getLwm2mObjects(sortOrder: SortOrder, objectIds?: string[], searchText?: string, config?: RequestConfig): Observable> { - let url = `/api/lwm2m/deviceProfile/?sortProperty=${sortOrder.property}&sortOrder=${sortOrder.direction}`; + let url = `/api/resource/lwm2m/?sortProperty=${sortOrder.property}&sortOrder=${sortOrder.direction}`; if (isDefinedAndNotNull(objectIds) && objectIds.length > 0) { url += `&objectIds=${objectIds}`; } @@ -63,9 +63,9 @@ export class DeviceProfileService { ); } - public getLwm2mObjectsPage(pageLink: PageLink, config?: RequestConfig): Observable> { - return this.http.get>( - `/api/lwm2m/deviceProfile/objects${pageLink.toQuery()}`, + public getLwm2mObjectsPage(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>( + `/api/resource/lwm2m/page${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config) ); } diff --git a/ui-ngx/src/app/core/http/resource.service.ts b/ui-ngx/src/app/core/http/resource.service.ts new file mode 100644 index 0000000000..d8bc18291b --- /dev/null +++ b/ui-ngx/src/app/core/http/resource.service.ts @@ -0,0 +1,81 @@ +/// +/// 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. +/// + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { PageLink } from '@shared/models/page/page-link'; +import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; +import { Observable } from 'rxjs'; +import { PageData } from '@shared/models/page/page-data'; +import { Resource, ResourceInfo } from '@shared/models/resource.models'; +import { map } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class ResourceService { + constructor( + private http: HttpClient + ) { + + } + + public getResources(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/resource${pageLink.toQuery()}`, + defaultHttpOptionsFromConfig(config)); + } + + public getResource(resourceId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/resource/${resourceId}`, defaultHttpOptionsFromConfig(config)); + } + + public downloadResource(resourceId: string): Observable { + return this.http.get(`/api/resource/${resourceId}/download`, { responseType: 'arraybuffer', observe: 'response' }).pipe( + map((response) => { + const headers = response.headers; + const filename = headers.get('x-filename'); + const contentType = headers.get('content-type'); + const linkElement = document.createElement('a'); + try { + const blob = new Blob([response.body], { type: contentType }); + const url = URL.createObjectURL(blob); + linkElement.setAttribute('href', url); + linkElement.setAttribute('download', filename); + const clickEvent = new MouseEvent('click', + { + view: window, + bubbles: true, + cancelable: false + } + ); + linkElement.dispatchEvent(clickEvent); + return null; + } catch (e) { + throw e; + } + }) + ); + } + + public saveResource(resource: Resource, config?: RequestConfig): Observable { + return this.http.post('/api/resource', resource, defaultHttpOptionsFromConfig(config)); + } + + public deleteResource(resourceId: string, config?: RequestConfig) { + return this.http.delete(`/api/resource/${resourceId}`, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index 85eda1037d..99ae8934c6 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -103,6 +103,13 @@ export class MenuService { path: '/widgets-bundles', icon: 'now_widgets' }, + { + id: guid(), + name: 'resource.resources-library', + type: 'link', + path: '/resources-library', + icon: 'folder' + }, { id: guid(), name: 'admin.system-settings', @@ -181,6 +188,16 @@ export class MenuService { } ] }, + { + name: 'resource.management', + places: [ + { + name: 'resource.resources-library', + icon: 'folder', + path: '/resources-library' + } + ] + }, { name: 'admin.system-settings', places: [ @@ -313,6 +330,13 @@ export class MenuService { path: '/dashboards', icon: 'dashboards' }, + { + id: guid(), + name: 'resource.resources-library', + type: 'link', + path: '/resources-library', + icon: 'folder' + }, { id: guid(), name: 'admin.home-settings', @@ -419,6 +443,16 @@ export class MenuService { ); } homeSections.push( + { + name: 'resource.management', + places: [ + { + name: 'resource.resources-library', + icon: 'folder', + path: '/resources-library' + } + ] + }, { name: 'dashboard.management', places: [ 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 55c2979fd9..faa2dbbce4 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 @@ -84,16 +84,18 @@ device.lwm2m-value - + {{ 'device.lwm2m-value-required' | translate }} - {{ 'device.lwm2m-value-json-error' | translate }} + {{ 'device.lwm2m-value-format-error' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html index 36dea4c42b..84797bcb98 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html @@ -20,14 +20,19 @@
-
- + + {{ 'device-profile.lwm2m.client-only-observe-after-connect-label' | translate }} + - {{ 'device-profile.lwm2m.client-only-observe-after-connect' | translate: - {count: +lwm2mDeviceProfileFormGroup.get('clientOnlyObserveAfterConnect').value} }} - + {{ 'device-profile.lwm2m.client-only-observe-after-connect' | translate: + {count: 1} }} + {{ 'device-profile.lwm2m.client-only-observe-after-connect' | translate: + {count: 2} }} + +
{ - const modelValue = {objectIds: null, objectsList: []} as ModelValue; + const modelValue = {objectIds: [], objectsList: []} as ModelValue; modelValue.objectIds = this.getObjectsFromJsonAllConfig(); - if (modelValue.objectIds !== null) { + if (modelValue.objectIds.length > 0) { const sortOrder = { property: 'id', direction: Direction.ASC @@ -172,7 +172,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro private updateObserveAttrTelemetryObjectFormGroup = (objectsList: ObjectLwM2M[]): void => { this.lwm2mDeviceProfileFormGroup.patchValue({ - observeAttrTelemetry: this.getObserveAttrTelemetryObjects(objectsList) + observeAttrTelemetry: deepClone(this.getObserveAttrTelemetryObjects(objectsList)) }, {emitEvent: false}); } @@ -195,32 +195,32 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro } } - private getObserveAttrTelemetryObjects = (listObject: ObjectLwM2M[]): object => { - const clientObserveAttrTelemetry = listObject; - if (this.configurationValue.observeAttr) { + private getObserveAttrTelemetryObjects = (objectList: ObjectLwM2M[]): object => { + const objectLwM2MS = deepClone(objectList); + if (this.configurationValue.observeAttr && objectLwM2MS.length > 0) { const observeArray = this.configurationValue.observeAttr.observe; const attributeArray = this.configurationValue.observeAttr.attribute; const telemetryArray = this.configurationValue.observeAttr.telemetry; let keyNameJson = this.configurationValue.observeAttr.keyName; if (this.includesNotZeroInstance(attributeArray, telemetryArray)) { - this.addInstances(attributeArray, telemetryArray, clientObserveAttrTelemetry); + this.addInstances(attributeArray, telemetryArray, objectLwM2MS); } - if (isDefinedAndNotNull(observeArray)) { - this.updateObserveAttrTelemetryObjects(observeArray, clientObserveAttrTelemetry, OBSERVE); + if (isDefinedAndNotNull(observeArray) && observeArray.length > 0) { + this.updateObserveAttrTelemetryObjects(observeArray, objectLwM2MS, OBSERVE); } - if (isDefinedAndNotNull(attributeArray)) { - this.updateObserveAttrTelemetryObjects(attributeArray, clientObserveAttrTelemetry, ATTRIBUTE); + if (isDefinedAndNotNull(attributeArray) && attributeArray.length > 0) { + this.updateObserveAttrTelemetryObjects(attributeArray, objectLwM2MS, ATTRIBUTE); } - if (isDefinedAndNotNull(telemetryArray)) { - this.updateObserveAttrTelemetryObjects(telemetryArray, clientObserveAttrTelemetry, TELEMETRY); + if (isDefinedAndNotNull(telemetryArray) && telemetryArray.length > 0) { + this.updateObserveAttrTelemetryObjects(telemetryArray, objectLwM2MS, TELEMETRY); } if (isDefinedAndNotNull(keyNameJson)) { this.configurationValue.observeAttr.keyName = this.validateKeyNameObjects(keyNameJson, attributeArray, telemetryArray); this.upDateJsonAllConfig(); - this.updateKeyNameObjects(clientObserveAttrTelemetry); + this.updateKeyNameObjects(objectLwM2MS); } } - return {clientLwM2M: clientObserveAttrTelemetry}; + return {clientLwM2M: objectLwM2MS}; } private includesNotZeroInstance = (attribute: string[], telemetry: string[]): boolean => { @@ -235,35 +235,39 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro .sort(this.sortPath); new Set(instancesPath).forEach(path => { - const pathParameter = Array.from(path.split('/'), Number); - const objectLwM2M = clientObserveAttrTelemetry.find(x => x.id === pathParameter[0]); + const pathParameter = Array.from(path.split('/'), String); + const objectLwM2M = clientObserveAttrTelemetry.find(x => x.keyId === pathParameter[0]); if (objectLwM2M) { const instance = deepClone(objectLwM2M.instances[0]); - instance.id = pathParameter[1]; + instance.id = +pathParameter[1]; objectLwM2M.instances.push(instance); } }); } - private updateObserveAttrTelemetryObjects = (parameters: string[], clientObserveAttrTelemetry: ObjectLwM2M[], + private updateObserveAttrTelemetryObjects = (parameters: string[], objectLwM2MS: ObjectLwM2M[], nameParameter: string): void => { parameters.forEach(parameter => { - const [objectId, instanceId, resourceId] = Array.from(parameter.substring(1).split('/'), Number); - clientObserveAttrTelemetry.find(objectLwm2m => objectLwm2m.id === objectId) - .instances.find(itrInstance => itrInstance.id === instanceId) - .resources.find(resource => resource.id === resourceId) - [nameParameter] = true; + const [objectKeyId, instanceId, resourceId] = Array.from(parameter.substring(1).split('/'), String); + const objectLwM2M = objectLwM2MS.find(objectLwm2m => objectLwm2m.keyId === objectKeyId); + if (objectLwM2M) { + objectLwM2M.instances.find(itrInstance => itrInstance.id === +instanceId) + .resources.find(resource => resource.id === +resourceId) + [nameParameter] = true; + } }); + } private updateKeyNameObjects = (clientObserveAttrTelemetry: ObjectLwM2M[]): void => { Object.keys(this.configurationValue.observeAttr.keyName).forEach(key => { - const [objectId, instanceId, resourceId] = Array.from(key.substring(1).split('/'), Number); - clientObserveAttrTelemetry.find(objectLwm2m => objectLwm2m.id === objectId) - .instances.find(instance => instance.id === instanceId) - .resources.find(resource => resource.id === resourceId) + const [objectKeyId, instanceId, resourceId] = Array.from(key.substring(1).split('/'), String); + const objectLwM2M = clientObserveAttrTelemetry.find(objectLwm2m => objectLwm2m.keyId === objectKeyId) + if (objectLwM2M) { + objectLwM2M.instances.find(instance => instance.id === +instanceId) + .resources.find(resource => resource.id === +resourceId) .keyName = this.configurationValue.observeAttr.keyName[key]; - }); + }}); } private validateKeyNameObjects = (nameJson: JsonObject, attributeArray: JsonArray, telemetryArray: JsonArray): {} => { @@ -290,7 +294,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro if (instance.hasOwnProperty(RESOURCES) && Array.isArray(instance.resources)) { instance.resources.forEach(resource => { if (resource.attribute || resource.telemetry) { - let pathRes = `/${obj.id}/${instance.id}/${resource.id}`; + let pathRes = `/${obj.keyId}/${instance.id}/${resource.id}`; if (resource.observe) { observeArray.push(pathRes); } @@ -345,26 +349,26 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro }); } - private getObjectsFromJsonAllConfig = (): number[] => { - const objectsIds = new Set(); + private getObjectsFromJsonAllConfig = (): string[] => { + const objectsIds = new Set(); if (this.configurationValue.observeAttr) { if (this.configurationValue.observeAttr.observe) { this.configurationValue.observeAttr.observe.forEach(obj => { - objectsIds.add(Array.from(obj.substring(1).split('/'), Number)[0]); + objectsIds.add(Array.from(obj.substring(1).split('/'), String)[0]); }); } if (this.configurationValue.observeAttr.attribute) { this.configurationValue.observeAttr.attribute.forEach(obj => { - objectsIds.add(Array.from(obj.substring(1).split('/'), Number)[0]); + objectsIds.add(Array.from(obj.substring(1).split('/'), String)[0]); }); } if (this.configurationValue.observeAttr.telemetry) { this.configurationValue.observeAttr.telemetry.forEach(obj => { - objectsIds.add(Array.from(obj.substring(1).split('/'), Number)[0]); + objectsIds.add(Array.from(obj.substring(1).split('/'), String)[0]); }); } } - return (objectsIds.size > 0) ? Array.from(objectsIds) : null; + return (objectsIds.size > 0) ? Array.from(objectsIds) : []; } private upDateJsonAllConfig = (): void => { @@ -379,21 +383,21 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro removeObjectsList = (value: ObjectLwM2M): void => { const objectsOld = this.lwm2mDeviceProfileFormGroup.get(OBSERVE_ATTR_TELEMETRY).value.clientLwM2M; - const isIdIndex = (element) => element.id === value.id; + const isIdIndex = (element) => element.keyId === value.keyId; const index = objectsOld.findIndex(isIdIndex); if (index >= 0) { objectsOld.splice(index, 1); } - this.removeObserveAttrTelemetryFromJson(OBSERVE, value.id); - this.removeObserveAttrTelemetryFromJson(TELEMETRY, value.id); - this.removeObserveAttrTelemetryFromJson(ATTRIBUTE, value.id); - this.removeKeyNameFromJson(value.id); + this.removeObserveAttrTelemetryFromJson(OBSERVE, value.keyId); + this.removeObserveAttrTelemetryFromJson(TELEMETRY, value.keyId); + this.removeObserveAttrTelemetryFromJson(ATTRIBUTE, value.keyId); + this.removeKeyNameFromJson(value.keyId); this.updateObserveAttrTelemetryObjectFormGroup(objectsOld); this.upDateJsonAllConfig(); } - private removeObserveAttrTelemetryFromJson = (observeAttrTel: string, id: number): void => { - const isIdIndex = (element) => element.startsWith(`/${id}`); + private removeObserveAttrTelemetryFromJson = (observeAttrTel: string, keyId: string): void => { + const isIdIndex = (element) => element.startsWith(`/${keyId}`); let index = this.configurationValue.observeAttr[observeAttrTel].findIndex(isIdIndex); while (index >= 0) { this.configurationValue.observeAttr[observeAttrTel].splice(index, 1); @@ -401,10 +405,10 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro } } - private removeKeyNameFromJson = (id: number): void => { + private removeKeyNameFromJson = (keyId: string): void => { const keyNameJson = this.configurationValue.observeAttr.keyName; Object.keys(keyNameJson).forEach(key => { - if (key.startsWith(`/${id}`)) { + if (key.startsWith(`/${keyId}`)) { delete keyNameJson[key]; } }); diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-list.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-list.component.ts index 1a0ab08e11..2b33f9cf3e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-list.component.ts @@ -14,12 +14,11 @@ /// limitations under the License. /// -import { Component, forwardRef, } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { INSTANCES_ID_VALUE_MAX, INSTANCES_ID_VALUE_MIN, KEY_REGEXP_NUMBER } from './profile-config.models'; -import { DeviceProfileService } from '@core/http/device-profile.service'; +import {Component, forwardRef,} from '@angular/core'; +import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators} from '@angular/forms'; +import {Store} from '@ngrx/store'; +import {AppState} from '@core/core.state'; +import {INSTANCES_ID_VALUE_MAX, INSTANCES_ID_VALUE_MIN, KEY_REGEXP_NUMBER} from './profile-config.models'; @Component({ selector: 'tb-profile-lwm2m-object-add-instances-list', @@ -43,7 +42,6 @@ export class Lwm2mObjectAddInstancesListComponent implements ControlValueAccesso private propagateChange = (v: any) => { }; constructor(private store: Store, - private deviceProfileService: DeviceProfileService, private fb: FormBuilder) { this.instancesListFormGroup = this.fb.group({ instanceIdInput: [null, [ diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances.component.html index 34a8d6a843..b37abd2c79 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances.component.html @@ -17,7 +17,7 @@ --> - {{data.objectName}}    (object [{{data.objectId}}]) + {{data.objectName}}    (object [{{data.objectKeyId}}])
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resource.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resource.component.ts index 678137140d..f3b2279f8d 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resource.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resource.component.ts @@ -14,13 +14,13 @@ /// limitations under the License. /// -import { Component, forwardRef, Input } from '@angular/core'; -import { ControlValueAccessor, FormArray, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; -import { ResourceLwM2M } from '@home/components/profile/device/lwm2m/profile-config.models'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; +import {Component, forwardRef, Input} from '@angular/core'; +import {ControlValueAccessor, FormArray, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators} from '@angular/forms'; +import {ResourceLwM2M} from '@home/components/profile/device/lwm2m/profile-config.models'; +import {Store} from '@ngrx/store'; +import {AppState} from '@core/core.state'; import _ from 'lodash'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import {coerceBooleanProperty} from '@angular/cdk/coercion'; @Component({ selector: 'tb-profile-lwm2m-observe-attr-telemetry-resource', @@ -95,7 +95,7 @@ export class Lwm2mObserveAttrTelemetryResourceComponent implements ControlValueA } else { this.resourceFormArray.clear(); resourcesLwM2M.forEach(resourceLwM2M => { - this.resourceFormArray.push(this.fb.group({ + this.resourceFormArray.push(this.fb.group( { id: resourceLwM2M.id, name: resourceLwM2M.name, observe: resourceLwM2M.observe, @@ -124,4 +124,10 @@ export class Lwm2mObserveAttrTelemetryResourceComponent implements ControlValueA trackByParams = (index: number): number => { return index; } + + updateObserve = (index: number): void =>{ + if (this.resourceFormArray.at(index).value.attribute === false && this.resourceFormArray.at(index).value.telemetry === false) { + this.resourceFormArray.at(index).patchValue({observe: false}); + } + } } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.html index 22f21ce9b7..9f84e6dc3e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.html @@ -22,7 +22,7 @@ [formGroupName]="i"> - {{ objectLwM2M.get('name').value}} <id: {{ objectLwM2M.get('id').value}}> + {{ objectLwM2M.get('name').value}} <id: {{ objectLwM2M.get('keyId').value}}> +
+
+ +
+ + resource.resource-type + + + {{ resourceTypesTranslationMap.get(resourceType) }} + + + + + resource.title + + + {{ 'resource.title-required' | translate }} + + + + +
+ +
diff --git a/ui-ngx/src/app/modules/home/pages/resource/resources-library.component.ts b/ui-ngx/src/app/modules/home/pages/resource/resources-library.component.ts new file mode 100644 index 0000000000..560b8a3d43 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/resource/resources-library.component.ts @@ -0,0 +1,122 @@ +/// +/// 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. +/// + +import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; +import { Subject } from 'rxjs'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { EntityComponent } from '@home/components/entity/entity.component'; +import { + Resource, + ResourceType, + ResourceTypeExtension, + ResourceTypeMIMETypes, + ResourceTypeTranslationMap +} from '@shared/models/resource.models'; +import { distinctUntilChanged, takeUntil } from 'rxjs/operators'; + +@Component({ + selector: 'tb-resources-library', + templateUrl: './resources-library.component.html' +}) +export class ResourcesLibraryComponent extends EntityComponent implements OnInit, OnDestroy { + + readonly resourceType = ResourceType; + readonly resourceTypes = Object.values(this.resourceType); + readonly resourceTypesTranslationMap = ResourceTypeTranslationMap; + + private destroy$ = new Subject(); + + constructor(protected store: Store, + protected translate: TranslateService, + @Inject('entity') protected entityValue: Resource, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + public fb: FormBuilder) { + super(store, fb, entityValue, entitiesTableConfigValue); + } + + ngOnInit() { + super.ngOnInit(); + this.entityForm.get('resourceType').valueChanges.pipe( + distinctUntilChanged((oldValue, newValue) => [oldValue, newValue].includes(this.resourceType.LWM2M_MODEL)), + takeUntil(this.destroy$) + ).subscribe((type) => { + if (type === this.resourceType.LWM2M_MODEL) { + this.entityForm.get('title').clearValidators(); + } else { + this.entityForm.get('title').setValidators(Validators.required); + } + this.entityForm.get('title').updateValueAndValidity({emitEvent: false}); + }); + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.destroy$.next(); + this.destroy$.complete(); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + buildForm(entity: Resource): FormGroup { + return this.fb.group( + { + resourceType: [{value: entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL, + disabled: this.isEdit }, [Validators.required]], + data: [entity ? entity.data : null, [Validators.required]], + fileName: [entity ? entity.fileName : null, [Validators.required]], + title: [entity ? entity.title : '', []] + } + ); + } + + updateForm(entity: Resource) { + this.entityForm.patchValue({resourceType: entity.resourceType}); + if (this.isEdit) { + this.entityForm.get('resourceType').disable({emitEvent: false}); + } + this.entityForm.patchValue({ + data: entity.data, + fileName: entity.fileName, + title: entity.title + }); + } + + getAllowedExtensions() { + try { + return ResourceTypeExtension.get(this.entityForm.get('resourceType').value); + } catch (e) { + return ''; + } + } + + getAcceptType() { + try { + return ResourceTypeMIMETypes.get(this.entityForm.get('resourceType').value); + } catch (e) { + return '*/*'; + } + } +} diff --git a/ui-ngx/src/app/shared/components/file-input.component.ts b/ui-ngx/src/app/shared/components/file-input.component.ts index 1dc64194d9..b91fba1def 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.ts +++ b/ui-ngx/src/app/shared/components/file-input.component.ts @@ -101,6 +101,9 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, @Input() existingFileName: string; + @Input() + convertToBase64 = false; + @Output() fileNameChanged = new EventEmitter(); @@ -128,7 +131,7 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, const reader = new FileReader(); reader.onload = (loadEvent) => { if (typeof reader.result === 'string') { - const fileContent = reader.result; + const fileContent = this.convertToBase64 ? window.btoa(reader.result) : reader.result; if (fileContent && fileContent.length > 0) { if (this.contentConvertFunction) { this.fileContent = this.contentConvertFunction(fileContent); @@ -144,7 +147,11 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, } } }; - reader.readAsText(file.file); + if (this.convertToBase64) { + reader.readAsBinaryString(file.file); + } else { + reader.readAsText(file.file); + } } } }); @@ -159,7 +166,9 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, } ngOnDestroy() { - this.autoUploadSubscription.unsubscribe(); + if (this.autoUploadSubscription) { + this.autoUploadSubscription.unsubscribe(); + } } registerOnChange(fn: any): void { diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index 1d587ccd72..52990127bf 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -35,8 +35,8 @@ export enum DeviceProfileType { export enum DeviceTransportType { DEFAULT = 'DEFAULT', MQTT = 'MQTT', - // LWM2M = 'LWM2M' - COAP = 'COAP' + COAP = 'COAP', + LWM2M = 'LWM2M' } export enum TransportPayloadType { @@ -82,8 +82,8 @@ export const deviceTransportTypeTranslationMap = new Map( [ [DeviceTransportType.DEFAULT, 'device-profile.transport-type-default-hint'], [DeviceTransportType.MQTT, 'device-profile.transport-type-mqtt-hint'], - // [DeviceTransportType.LWM2M, 'device-profile.transport-type-lwm2m-hint'] - [DeviceTransportType.COAP, 'device-profile.transport-type-coap-hint'] + [DeviceTransportType.COAP, 'device-profile.transport-type-coap-hint'], + [DeviceTransportType.LWM2M, 'device-profile.transport-type-lwm2m-hint'] ] ); @@ -164,15 +164,8 @@ export const deviceTransportTypeConfigurationInfoMap = new Map( [ [DeviceCredentialsType.ACCESS_TOKEN, 'Access token'], - [DeviceCredentialsType.X509_CERTIFICATE, 'MQTT X.509'], + [DeviceCredentialsType.X509_CERTIFICATE, 'X.509'], [DeviceCredentialsType.MQTT_BASIC, 'MQTT Basic'], [DeviceCredentialsType.LWM2M_CREDENTIALS, 'LwM2M Credentials'] ] diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 53fc336e53..33e03b388e 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -17,22 +17,6 @@ import { TenantId } from './id/tenant-id'; import { BaseData, HasId } from '@shared/models/base-data'; -/// -/// Copyright © 2016-2019 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. -/// - export enum EntityType { TENANT = 'TENANT', TENANT_PROFILE = 'TENANT_PROFILE', @@ -49,7 +33,8 @@ export enum EntityType { ENTITY_VIEW = 'ENTITY_VIEW', WIDGETS_BUNDLE = 'WIDGETS_BUNDLE', WIDGET_TYPE = 'WIDGET_TYPE', - API_USAGE_STATE = 'API_USAGE_STATE' + API_USAGE_STATE = 'API_USAGE_STATE', + TB_RESOURCE = 'TB_RESOURCE' } export enum AliasEntityType { @@ -297,7 +282,17 @@ export const entityTypeTranslations = new Map( + [ + [ResourceType.LWM2M_MODEL, 'application/xml,text/xml'], + [ResourceType.PKCS_12, 'application/x-pkcs12'], + [ResourceType.JKS, 'application/x-java-keystore'] + ] +); + +export const ResourceTypeExtension = new Map( + [ + [ResourceType.LWM2M_MODEL, 'xml'], + [ResourceType.PKCS_12, 'p12,pfx'], + [ResourceType.JKS, 'jks'] + ] +); + +export const ResourceTypeTranslationMap = new Map( + [ + [ResourceType.LWM2M_MODEL, 'LWM2M model'], + [ResourceType.PKCS_12, 'PKCS #12'], + [ResourceType.JKS, 'JKS'] + ] +); + +export interface ResourceInfo extends BaseData { + tenantId?: TenantId; + resourceKey?: string; + title?: string; + resourceType: ResourceType; +} + +export interface Resource extends ResourceInfo { + data: string; + fileName: string; +} 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 5ca29d0af4..7a5bcda9c8 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -919,11 +919,11 @@ "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-json-error": "LwM2M Security config value is not json format.", + "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-value-edit-tip": "Edit security config json editor", + "lwm2m-credentials-value-tip": "Edit security config json editor", "lwm2m-security-config": { "identity": "Client Identity", "client-key": "Client Key", @@ -1150,8 +1150,15 @@ "schedule-time-from": "From", "schedule-time-to": "To", "schedule-days-of-week-required": "At least one day of week should be selected.", + "create-device-profile": "Create new device profile", + "import": "Import device profile", + "export": "Export device profile", + "export-failed-error": "Unable to export device profile: {{error}}", + "device-profile-file": "Device profile file", + "invalid-device-profile-file-error": "Unable to import device profile: Invalid device profile data structure.", "lwm2m": { - "client-only-observe-after-connect": "{ count, plural, 1 {Strategy 1: Only Observe Request to the client after the initial connection} other {Strategy 2: Read All Resources & Observe Request to the client after registration} }", + "client-only-observe-after-connect-label": "Strategy", + "client-only-observe-after-connect": "{ count, plural, 1 {1: Only Observe Request to the client after the initial connection} other {2: Read All Resources & Observe Request to the client after registration} }", "client-only-observe-after-connect-tip": "{ count, plural, 1 {Strategy 1: After the initial connection of the LWM2M Client, the server sends Observe resources Request to the client, those resources that are marked as observation in the Device profile and which exist on the LWM2M client.} other {Strategy 2: After the registration, request the client to read all the resource values for all objects that the LWM2M client has,\n then execute: the server sends Observe resources Request to the client, those resources that are marked as observation in the Device profile and which exist on the LWM2M client.} }", "object-list": "Object list", "object-list-empty": "No objects selected.", @@ -1199,13 +1206,7 @@ "bootstrap-server-account-timeout": "Account after the timeout", "bootstrap-server-account-timeout-tip": "Bootstrap-Server Account after the timeout value given by this resource.", "config-json-tab": "Json Config Profile Device" - }, - "create-device-profile": "Create new device profile", - "import": "Import device profile", - "export": "Export device profile", - "export-failed-error": "Unable to export device profile: {{error}}", - "device-profile-file": "Device profile file", - "invalid-device-profile-file-error": "Unable to import device profile: Invalid device profile data structure." + } }, "dialog": { "close": "Close dialog" @@ -2148,6 +2149,31 @@ "invalid-additional-info": "Unable to parse additional info json.", "no-relations-text": "No relations found" }, + "resource": { + "add": "Add Resource", + "delete": "Delete resource", + "delete-resource-text": "Be careful, after the confirmation the resource will become unrecoverable.", + "delete-resource-title": "Are you sure you want to delete the resource '{{resourceTitle}}'?", + "delete-resources-action-title": "Delete { count, plural, 1 {1 resource} other {# resources} }", + "delete-resources-text": "Be careful, after the confirmation all selected resources will be removed.", + "delete-resources-title": "Are you sure you want to delete { count, plural, 1 {1 resource} other {# resources} }?", + "drop-file": "Drop a resource file or click to select a file to upload.", + "empty": "Resource is empty", + "export": "Export resource", + "management": "Resource management", + "no-resource-matching": "No resource matching '{{widgetsBundle}}' were found.", + "no-resource-text": "No resources found", + "open-widgets-bundle": "Open widgets bundle", + "resource": "Resource", + "resource-library-details": "Resource library details", + "resource-type": "Resource type", + "resources-library": "Resources library", + "search": "Search resources", + "selected-resources": "{ count, plural, 1 {1 resource} other {# resources} } selected", + "system": "System", + "title": "Title", + "title-required": "Title is required." + }, "rulechain": { "rulechain": "Rule chain", "rulechains": "Rule chains",