Commit 6cdb508c71041fb15757ef2e7d9182e48ea18601
Committed by
GitHub
1 parent
fcb0c041
Claiming devices implementation
Showing
30 changed files
with
743 additions
and
109 deletions
... | ... | @@ -28,7 +28,16 @@ import org.springframework.security.core.Authentication; |
28 | 28 | import org.springframework.security.core.context.SecurityContextHolder; |
29 | 29 | import org.springframework.web.bind.annotation.ExceptionHandler; |
30 | 30 | import org.thingsboard.server.actors.service.ActorService; |
31 | -import org.thingsboard.server.common.data.*; | |
31 | +import org.thingsboard.server.common.data.Customer; | |
32 | +import org.thingsboard.server.common.data.Dashboard; | |
33 | +import org.thingsboard.server.common.data.DashboardInfo; | |
34 | +import org.thingsboard.server.common.data.DataConstants; | |
35 | +import org.thingsboard.server.common.data.Device; | |
36 | +import org.thingsboard.server.common.data.EntityType; | |
37 | +import org.thingsboard.server.common.data.EntityView; | |
38 | +import org.thingsboard.server.common.data.HasName; | |
39 | +import org.thingsboard.server.common.data.Tenant; | |
40 | +import org.thingsboard.server.common.data.User; | |
32 | 41 | import org.thingsboard.server.common.data.alarm.Alarm; |
33 | 42 | import org.thingsboard.server.common.data.alarm.AlarmId; |
34 | 43 | import org.thingsboard.server.common.data.alarm.AlarmInfo; |
... | ... | @@ -36,7 +45,19 @@ import org.thingsboard.server.common.data.asset.Asset; |
36 | 45 | import org.thingsboard.server.common.data.audit.ActionType; |
37 | 46 | import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; |
38 | 47 | import org.thingsboard.server.common.data.exception.ThingsboardException; |
39 | -import org.thingsboard.server.common.data.id.*; | |
48 | +import org.thingsboard.server.common.data.id.AssetId; | |
49 | +import org.thingsboard.server.common.data.id.CustomerId; | |
50 | +import org.thingsboard.server.common.data.id.DashboardId; | |
51 | +import org.thingsboard.server.common.data.id.DeviceId; | |
52 | +import org.thingsboard.server.common.data.id.EntityId; | |
53 | +import org.thingsboard.server.common.data.id.EntityIdFactory; | |
54 | +import org.thingsboard.server.common.data.id.EntityViewId; | |
55 | +import org.thingsboard.server.common.data.id.RuleChainId; | |
56 | +import org.thingsboard.server.common.data.id.RuleNodeId; | |
57 | +import org.thingsboard.server.common.data.id.TenantId; | |
58 | +import org.thingsboard.server.common.data.id.UserId; | |
59 | +import org.thingsboard.server.common.data.id.WidgetTypeId; | |
60 | +import org.thingsboard.server.common.data.id.WidgetsBundleId; | |
40 | 61 | import org.thingsboard.server.common.data.kv.AttributeKvEntry; |
41 | 62 | import org.thingsboard.server.common.data.kv.DataType; |
42 | 63 | import org.thingsboard.server.common.data.page.TextPageLink; |
... | ... | @@ -45,7 +66,6 @@ import org.thingsboard.server.common.data.plugin.ComponentDescriptor; |
45 | 66 | import org.thingsboard.server.common.data.plugin.ComponentType; |
46 | 67 | import org.thingsboard.server.common.data.rule.RuleChain; |
47 | 68 | import org.thingsboard.server.common.data.rule.RuleNode; |
48 | -import org.thingsboard.server.common.data.security.Authority; | |
49 | 69 | import org.thingsboard.server.common.data.widget.WidgetType; |
50 | 70 | import org.thingsboard.server.common.data.widget.WidgetsBundle; |
51 | 71 | import org.thingsboard.server.common.msg.TbMsg; |
... | ... | @@ -53,13 +73,13 @@ import org.thingsboard.server.common.msg.TbMsgDataType; |
53 | 73 | import org.thingsboard.server.common.msg.TbMsgMetaData; |
54 | 74 | import org.thingsboard.server.common.msg.cluster.SendToClusterMsg; |
55 | 75 | import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg; |
56 | -import org.thingsboard.server.common.msg.tools.TbRateLimitsException; | |
57 | 76 | import org.thingsboard.server.dao.alarm.AlarmService; |
58 | 77 | import org.thingsboard.server.dao.asset.AssetService; |
59 | 78 | import org.thingsboard.server.dao.attributes.AttributesService; |
60 | 79 | import org.thingsboard.server.dao.audit.AuditLogService; |
61 | 80 | import org.thingsboard.server.dao.customer.CustomerService; |
62 | 81 | import org.thingsboard.server.dao.dashboard.DashboardService; |
82 | +import org.thingsboard.server.dao.device.ClaimDevicesService; | |
63 | 83 | import org.thingsboard.server.dao.device.DeviceCredentialsService; |
64 | 84 | import org.thingsboard.server.dao.device.DeviceService; |
65 | 85 | import org.thingsboard.server.dao.entityview.EntityViewService; |
... | ... | @@ -162,6 +182,9 @@ public abstract class BaseController { |
162 | 182 | @Autowired |
163 | 183 | protected AttributesService attributesService; |
164 | 184 | |
185 | + @Autowired | |
186 | + protected ClaimDevicesService claimDevicesService; | |
187 | + | |
165 | 188 | @Value("${server.log_controller_error_stack_trace}") |
166 | 189 | @Getter |
167 | 190 | private boolean logControllerErrorStackTrace; | ... | ... |
... | ... | @@ -15,8 +15,11 @@ |
15 | 15 | */ |
16 | 16 | package org.thingsboard.server.controller; |
17 | 17 | |
18 | +import com.google.common.util.concurrent.FutureCallback; | |
19 | +import com.google.common.util.concurrent.Futures; | |
18 | 20 | import com.google.common.util.concurrent.ListenableFuture; |
19 | 21 | import org.springframework.http.HttpStatus; |
22 | +import org.springframework.http.ResponseEntity; | |
20 | 23 | import org.springframework.security.access.prepost.PreAuthorize; |
21 | 24 | import org.springframework.web.bind.annotation.PathVariable; |
22 | 25 | import org.springframework.web.bind.annotation.RequestBody; |
... | ... | @@ -26,27 +29,31 @@ import org.springframework.web.bind.annotation.RequestParam; |
26 | 29 | import org.springframework.web.bind.annotation.ResponseBody; |
27 | 30 | import org.springframework.web.bind.annotation.ResponseStatus; |
28 | 31 | import org.springframework.web.bind.annotation.RestController; |
32 | +import org.springframework.web.context.request.async.DeferredResult; | |
29 | 33 | import org.thingsboard.server.common.data.Customer; |
34 | +import org.thingsboard.server.common.data.DataConstants; | |
30 | 35 | import org.thingsboard.server.common.data.Device; |
31 | 36 | import org.thingsboard.server.common.data.EntitySubtype; |
32 | 37 | import org.thingsboard.server.common.data.EntityType; |
33 | 38 | import org.thingsboard.server.common.data.audit.ActionType; |
34 | 39 | import org.thingsboard.server.common.data.device.DeviceSearchQuery; |
35 | -import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; | |
36 | 40 | import org.thingsboard.server.common.data.exception.ThingsboardException; |
37 | 41 | import org.thingsboard.server.common.data.id.CustomerId; |
38 | 42 | import org.thingsboard.server.common.data.id.DeviceId; |
39 | 43 | import org.thingsboard.server.common.data.id.TenantId; |
40 | 44 | import org.thingsboard.server.common.data.page.TextPageData; |
41 | 45 | import org.thingsboard.server.common.data.page.TextPageLink; |
42 | -import org.thingsboard.server.common.data.security.Authority; | |
43 | 46 | import org.thingsboard.server.common.data.security.DeviceCredentials; |
47 | +import org.thingsboard.server.controller.claim.data.ClaimRequest; | |
48 | +import org.thingsboard.server.dao.device.claim.ClaimResponse; | |
44 | 49 | import org.thingsboard.server.dao.exception.IncorrectParameterException; |
45 | 50 | import org.thingsboard.server.dao.model.ModelConstants; |
46 | 51 | import org.thingsboard.server.service.security.model.SecurityUser; |
47 | 52 | import org.thingsboard.server.service.security.permission.Operation; |
48 | 53 | import org.thingsboard.server.service.security.permission.Resource; |
49 | 54 | |
55 | +import javax.annotation.Nullable; | |
56 | +import java.io.IOException; | |
50 | 57 | import java.util.ArrayList; |
51 | 58 | import java.util.List; |
52 | 59 | import java.util.stream.Collectors; |
... | ... | @@ -55,7 +62,8 @@ import java.util.stream.Collectors; |
55 | 62 | @RequestMapping("/api") |
56 | 63 | public class DeviceController extends BaseController { |
57 | 64 | |
58 | - public static final String DEVICE_ID = "deviceId"; | |
65 | + private static final String DEVICE_ID = "deviceId"; | |
66 | + private static final String DEVICE_NAME = "deviceName"; | |
59 | 67 | |
60 | 68 | @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") |
61 | 69 | @RequestMapping(value = "/device/{deviceId}", method = RequestMethod.GET) |
... | ... | @@ -379,4 +387,91 @@ public class DeviceController extends BaseController { |
379 | 387 | throw handleException(e); |
380 | 388 | } |
381 | 389 | } |
390 | + | |
391 | + @PreAuthorize("hasAnyAuthority('CUSTOMER_USER')") | |
392 | + @RequestMapping(value = "/customer/device/{deviceName}/claim", method = RequestMethod.POST) | |
393 | + @ResponseBody | |
394 | + public DeferredResult<ResponseEntity> claimDevice(@PathVariable(DEVICE_NAME) String deviceName, | |
395 | + @RequestBody(required = false) ClaimRequest claimRequest) throws ThingsboardException { | |
396 | + checkParameter(DEVICE_NAME, deviceName); | |
397 | + try { | |
398 | + final DeferredResult<ResponseEntity> deferredResult = new DeferredResult<>(); | |
399 | + | |
400 | + SecurityUser user = getCurrentUser(); | |
401 | + TenantId tenantId = user.getTenantId(); | |
402 | + CustomerId customerId = user.getCustomerId(); | |
403 | + | |
404 | + Device device = checkNotNull(deviceService.findDeviceByTenantIdAndName(tenantId, deviceName)); | |
405 | + accessControlService.checkPermission(user, Resource.DEVICE, Operation.CLAIM_DEVICES, | |
406 | + device.getId(), device); | |
407 | + String secretKey = getSecretKey(claimRequest); | |
408 | + | |
409 | + ListenableFuture<ClaimResponse> future = claimDevicesService.claimDevice(device, customerId, secretKey); | |
410 | + Futures.addCallback(future, new FutureCallback<ClaimResponse>() { | |
411 | + @Override | |
412 | + public void onSuccess(@Nullable ClaimResponse result) { | |
413 | + HttpStatus status; | |
414 | + if (result.equals(ClaimResponse.SUCCESS)) { | |
415 | + status = HttpStatus.OK; | |
416 | + } else { | |
417 | + status = HttpStatus.BAD_REQUEST; | |
418 | + } | |
419 | + deferredResult.setResult(new ResponseEntity<>(result, status)); | |
420 | + } | |
421 | + | |
422 | + @Override | |
423 | + public void onFailure(Throwable t) { | |
424 | + deferredResult.setErrorResult(t); | |
425 | + } | |
426 | + }); | |
427 | + return deferredResult; | |
428 | + } catch (Exception e) { | |
429 | + throw handleException(e); | |
430 | + } | |
431 | + } | |
432 | + | |
433 | + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") | |
434 | + @RequestMapping(value = "/customer/device/{deviceName}/claim", method = RequestMethod.DELETE) | |
435 | + @ResponseStatus(value = HttpStatus.OK) | |
436 | + public DeferredResult<ResponseEntity> reClaimDevice(@PathVariable(DEVICE_NAME) String deviceName) throws ThingsboardException { | |
437 | + checkParameter(DEVICE_NAME, deviceName); | |
438 | + try { | |
439 | + final DeferredResult<ResponseEntity> deferredResult = new DeferredResult<>(); | |
440 | + | |
441 | + SecurityUser user = getCurrentUser(); | |
442 | + TenantId tenantId = user.getTenantId(); | |
443 | + | |
444 | + Device device = checkNotNull(deviceService.findDeviceByTenantIdAndName(tenantId, deviceName)); | |
445 | + accessControlService.checkPermission(user, Resource.DEVICE, Operation.CLAIM_DEVICES, | |
446 | + device.getId(), device); | |
447 | + | |
448 | + ListenableFuture<List<Void>> future = claimDevicesService.reClaimDevice(tenantId, device); | |
449 | + Futures.addCallback(future, new FutureCallback<List<Void>>() { | |
450 | + @Override | |
451 | + public void onSuccess(@Nullable List<Void> result) { | |
452 | + if (result != null) { | |
453 | + deferredResult.setResult(new ResponseEntity(HttpStatus.OK)); | |
454 | + } else { | |
455 | + deferredResult.setResult(new ResponseEntity(HttpStatus.BAD_REQUEST)); | |
456 | + } | |
457 | + } | |
458 | + | |
459 | + @Override | |
460 | + public void onFailure(Throwable t) { | |
461 | + deferredResult.setErrorResult(t); | |
462 | + } | |
463 | + }); | |
464 | + return deferredResult; | |
465 | + } catch (Exception e) { | |
466 | + throw handleException(e); | |
467 | + } | |
468 | + } | |
469 | + | |
470 | + private String getSecretKey(ClaimRequest claimRequest) throws IOException { | |
471 | + String secretKey = claimRequest.getSecretKey(); | |
472 | + if (secretKey != null) { | |
473 | + return secretKey; | |
474 | + } | |
475 | + return DataConstants.DEFAULT_SECRET_KEY; | |
476 | + } | |
382 | 477 | } | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.thingsboard.server.controller.claim.data; | |
17 | + | |
18 | +import lombok.Data; | |
19 | + | |
20 | +@Data | |
21 | +public class ClaimRequest { | |
22 | + | |
23 | + private final String secretKey; | |
24 | + | |
25 | +} | ... | ... |
application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java
renamed from
application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPremissions.java
... | ... | @@ -16,20 +16,20 @@ |
16 | 16 | package org.thingsboard.server.service.security.permission; |
17 | 17 | |
18 | 18 | import org.springframework.stereotype.Component; |
19 | -import org.thingsboard.server.common.data.*; | |
19 | +import org.thingsboard.server.common.data.DashboardInfo; | |
20 | +import org.thingsboard.server.common.data.HasCustomerId; | |
21 | +import org.thingsboard.server.common.data.HasTenantId; | |
22 | +import org.thingsboard.server.common.data.User; | |
20 | 23 | import org.thingsboard.server.common.data.id.DashboardId; |
21 | 24 | import org.thingsboard.server.common.data.id.EntityId; |
22 | -import org.thingsboard.server.common.data.id.TenantId; | |
23 | 25 | import org.thingsboard.server.common.data.id.UserId; |
24 | 26 | import org.thingsboard.server.common.data.security.Authority; |
25 | 27 | import org.thingsboard.server.service.security.model.SecurityUser; |
26 | 28 | |
27 | -import java.util.HashMap; | |
29 | +@Component(value = "customerUserPermissions") | |
30 | +public class CustomerUserPermissions extends AbstractPermissions { | |
28 | 31 | |
29 | -@Component(value="customerUserPermissions") | |
30 | -public class CustomerUserPremissions extends AbstractPermissions { | |
31 | - | |
32 | - public CustomerUserPremissions() { | |
32 | + public CustomerUserPermissions() { | |
33 | 33 | super(); |
34 | 34 | put(Resource.ALARM, TenantAdminPermissions.tenantEntityPermissionChecker); |
35 | 35 | put(Resource.ASSET, customerEntityPermissionChecker); |
... | ... | @@ -44,26 +44,26 @@ public class CustomerUserPremissions extends AbstractPermissions { |
44 | 44 | |
45 | 45 | private static final PermissionChecker customerEntityPermissionChecker = |
46 | 46 | new PermissionChecker.GenericPermissionChecker(Operation.READ, Operation.READ_CREDENTIALS, |
47 | - Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY, Operation.RPC_CALL) { | |
47 | + Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY, Operation.RPC_CALL, Operation.CLAIM_DEVICES) { | |
48 | 48 | |
49 | - @Override | |
50 | - public boolean hasPermission(SecurityUser user, Operation operation, EntityId entityId, HasTenantId entity) { | |
49 | + @Override | |
50 | + public boolean hasPermission(SecurityUser user, Operation operation, EntityId entityId, HasTenantId entity) { | |
51 | 51 | |
52 | - if (!super.hasPermission(user, operation, entityId, entity)) { | |
53 | - return false; | |
54 | - } | |
55 | - if (!user.getTenantId().equals(entity.getTenantId())) { | |
56 | - return false; | |
57 | - } | |
58 | - if (!(entity instanceof HasCustomerId)) { | |
59 | - return false; | |
60 | - } | |
61 | - if (!user.getCustomerId().equals(((HasCustomerId)entity).getCustomerId())) { | |
62 | - return false; | |
63 | - } | |
64 | - return true; | |
65 | - } | |
66 | - }; | |
52 | + if (!super.hasPermission(user, operation, entityId, entity)) { | |
53 | + return false; | |
54 | + } | |
55 | + if (!user.getTenantId().equals(entity.getTenantId())) { | |
56 | + return false; | |
57 | + } | |
58 | + if (!(entity instanceof HasCustomerId)) { | |
59 | + return false; | |
60 | + } | |
61 | + if (!operation.equals(Operation.CLAIM_DEVICES) && !user.getCustomerId().equals(((HasCustomerId) entity).getCustomerId())) { | |
62 | + return false; | |
63 | + } | |
64 | + return true; | |
65 | + } | |
66 | + }; | |
67 | 67 | |
68 | 68 | private static final PermissionChecker customerPermissionChecker = |
69 | 69 | new PermissionChecker.GenericPermissionChecker(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY) { | ... | ... |
... | ... | @@ -18,6 +18,6 @@ package org.thingsboard.server.service.security.permission; |
18 | 18 | public enum Operation { |
19 | 19 | |
20 | 20 | ALL, CREATE, READ, WRITE, DELETE, ASSIGN_TO_CUSTOMER, UNASSIGN_FROM_CUSTOMER, RPC_CALL, |
21 | - READ_CREDENTIALS, WRITE_CREDENTIALS, READ_ATTRIBUTES, WRITE_ATTRIBUTES, READ_TELEMETRY, WRITE_TELEMETRY | |
21 | + READ_CREDENTIALS, WRITE_CREDENTIALS, READ_ATTRIBUTES, WRITE_ATTRIBUTES, READ_TELEMETRY, WRITE_TELEMETRY, CLAIM_DEVICES | |
22 | 22 | |
23 | 23 | } | ... | ... |
... | ... | @@ -22,11 +22,31 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
22 | 22 | import org.springframework.stereotype.Service; |
23 | 23 | import org.thingsboard.rule.engine.api.util.DonAsynchron; |
24 | 24 | import org.thingsboard.server.actors.ActorSystemContext; |
25 | +import org.thingsboard.server.common.data.id.DeviceId; | |
26 | +import org.thingsboard.server.common.data.id.TenantId; | |
25 | 27 | import org.thingsboard.server.common.msg.cluster.ServerAddress; |
26 | 28 | import org.thingsboard.server.common.transport.TransportServiceCallback; |
27 | 29 | import org.thingsboard.server.common.transport.service.AbstractTransportService; |
30 | +import org.thingsboard.server.dao.device.ClaimDevicesService; | |
28 | 31 | import org.thingsboard.server.gen.transport.TransportProtos; |
29 | -import org.thingsboard.server.gen.transport.TransportProtos.*; | |
32 | +import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg; | |
33 | +import org.thingsboard.server.gen.transport.TransportProtos.DeviceActorToTransportMsg; | |
34 | +import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg; | |
35 | +import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; | |
36 | +import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg; | |
37 | +import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg; | |
38 | +import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg; | |
39 | +import org.thingsboard.server.gen.transport.TransportProtos.SessionEventMsg; | |
40 | +import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; | |
41 | +import org.thingsboard.server.gen.transport.TransportProtos.SubscribeToAttributeUpdatesMsg; | |
42 | +import org.thingsboard.server.gen.transport.TransportProtos.SubscribeToRPCMsg; | |
43 | +import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseMsg; | |
44 | +import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcRequestMsg; | |
45 | +import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; | |
46 | +import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg; | |
47 | +import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; | |
48 | +import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; | |
49 | +import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; | |
30 | 50 | import org.thingsboard.server.service.cluster.routing.ClusterRoutingService; |
31 | 51 | import org.thingsboard.server.service.cluster.rpc.ClusterRpcService; |
32 | 52 | import org.thingsboard.server.service.encoding.DataDecodingEncodingService; |
... | ... | @@ -35,6 +55,7 @@ import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWra |
35 | 55 | import javax.annotation.PostConstruct; |
36 | 56 | import javax.annotation.PreDestroy; |
37 | 57 | import java.util.Optional; |
58 | +import java.util.UUID; | |
38 | 59 | import java.util.function.Consumer; |
39 | 60 | |
40 | 61 | /** |
... | ... | @@ -58,6 +79,8 @@ public class LocalTransportService extends AbstractTransportService implements R |
58 | 79 | private ClusterRpcService rpcService; |
59 | 80 | @Autowired |
60 | 81 | private DataDecodingEncodingService encodingService; |
82 | + @Autowired | |
83 | + private ClaimDevicesService claimDevicesService; | |
61 | 84 | |
62 | 85 | @PostConstruct |
63 | 86 | public void init() { |
... | ... | @@ -151,6 +174,23 @@ public class LocalTransportService extends AbstractTransportService implements R |
151 | 174 | } |
152 | 175 | |
153 | 176 | @Override |
177 | + protected void registerClaimingInfo(SessionInfoProto sessionInfo, ClaimDeviceMsg msg, TransportServiceCallback<Void> callback) { | |
178 | + TransportToDeviceActorMsg toDeviceActorMsg = TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo).setClaimDevice(msg).build(); | |
179 | + | |
180 | + TransportToDeviceActorMsgWrapper wrapper = new TransportToDeviceActorMsgWrapper(toDeviceActorMsg); | |
181 | + Optional<ServerAddress> address = routingService.resolveById(wrapper.getDeviceId()); | |
182 | + if (address.isPresent()) { | |
183 | + rpcService.tell(encodingService.convertToProtoDataMessage(address.get(), wrapper)); | |
184 | + callback.onSuccess(null); | |
185 | + } else { | |
186 | + TenantId tenantId = new TenantId(new UUID(sessionInfo.getTenantIdMSB(), sessionInfo.getTenantIdLSB())); | |
187 | + DeviceId deviceId = new DeviceId(new UUID(msg.getDeviceIdMSB(), msg.getDeviceIdLSB())); | |
188 | + DonAsynchron.withCallback(claimDevicesService.registerClaimingInfo(tenantId, deviceId, msg.getSecretKey(), msg.getDurationMs()), | |
189 | + callback::onSuccess, callback::onError); | |
190 | + } | |
191 | + } | |
192 | + | |
193 | + @Override | |
154 | 194 | public void process(String nodeId, DeviceActorToTransportMsg msg) { |
155 | 195 | process(nodeId, msg, null, null); |
156 | 196 | } | ... | ... |
... | ... | @@ -103,6 +103,11 @@ security: |
103 | 103 | user_token_access_enabled: "${SECURITY_USER_TOKEN_ACCESS_ENABLED:true}" |
104 | 104 | # Enable/disable case-sensitive username login |
105 | 105 | user_login_case_sensitive: "${SECURITY_USER_LOGIN_CASE_SENSITIVE:true}" |
106 | + claim: | |
107 | + # Enable/disable claiming devices, if false -> the device's [claimingAllowed] SERVER_SCOPE attribute must be set to [true] to allow claiming specific device | |
108 | + allowClaimingByDefault: "${SECURITY_CLAIM_ALLOW_CLAIMING_BY_DEFAULT:true}" | |
109 | + # Time allowed to claim the device in milliseconds | |
110 | + duration: "${SECURITY_CLAIM_DURATION:60000}" # 1 minute, note this value must equal claimDevices.timeToLiveInMinutes value | |
106 | 111 | |
107 | 112 | # Dashboard parameters |
108 | 113 | dashboard: |
... | ... | @@ -261,6 +266,9 @@ caffeine: |
261 | 266 | entityViews: |
262 | 267 | timeToLiveInMinutes: 1440 |
263 | 268 | maxSize: 100000 |
269 | + claimDevices: | |
270 | + timeToLiveInMinutes: 1 | |
271 | + maxSize: 100000 | |
264 | 272 | |
265 | 273 | redis: |
266 | 274 | # standalone or cluster | ... | ... |
... | ... | @@ -22,4 +22,5 @@ public class CacheConstants { |
22 | 22 | public static final String SESSIONS_CACHE = "sessions"; |
23 | 23 | public static final String ASSET_CACHE = "assets"; |
24 | 24 | public static final String ENTITY_VIEW_CACHE = "entityViews"; |
25 | + public static final String CLAIM_DEVICES_CACHE = "claimDevices"; | |
25 | 26 | } | ... | ... |
... | ... | @@ -59,4 +59,8 @@ public class DataConstants { |
59 | 59 | |
60 | 60 | public static final String RPC_CALL_FROM_SERVER_TO_DEVICE = "RPC_CALL_FROM_SERVER_TO_DEVICE"; |
61 | 61 | |
62 | + public static final String DEFAULT_SECRET_KEY = ""; | |
63 | + public static final String SECRET_KEY_FIELD_NAME = "secretKey"; | |
64 | + public static final String DURATION_MS_FIELD_NAME = "durationMs"; | |
65 | + | |
62 | 66 | } | ... | ... |
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
... | ... | @@ -19,7 +19,6 @@ import lombok.extern.slf4j.Slf4j; |
19 | 19 | import org.eclipse.californium.core.CoapResource; |
20 | 20 | import org.eclipse.californium.core.coap.CoAP.ResponseCode; |
21 | 21 | import org.eclipse.californium.core.coap.Request; |
22 | -import org.eclipse.californium.core.coap.Response; | |
23 | 22 | import org.eclipse.californium.core.network.Exchange; |
24 | 23 | import org.eclipse.californium.core.network.ExchangeObserver; |
25 | 24 | import org.eclipse.californium.core.server.resources.CoapExchange; |
... | ... | @@ -122,6 +121,9 @@ public class CoapTransportResource extends CoapResource { |
122 | 121 | processRequest(exchange, SessionMsgType.TO_SERVER_RPC_REQUEST); |
123 | 122 | } |
124 | 123 | break; |
124 | + case CLAIM: | |
125 | + processRequest(exchange, SessionMsgType.CLAIM_REQUEST); | |
126 | + break; | |
125 | 127 | } |
126 | 128 | } |
127 | 129 | } |
... | ... | @@ -153,6 +155,11 @@ public class CoapTransportResource extends CoapResource { |
153 | 155 | transportContext.getAdaptor().convertToPostTelemetry(sessionId, request), |
154 | 156 | new CoapOkCallback(exchange)); |
155 | 157 | break; |
158 | + case CLAIM_REQUEST: | |
159 | + transportService.process(sessionInfo, | |
160 | + transportContext.getAdaptor().convertToClaimDevice(sessionId, request, sessionInfo), | |
161 | + new CoapOkCallback(exchange)); | |
162 | + break; | |
156 | 163 | case SUBSCRIBE_ATTRIBUTES_REQUEST: |
157 | 164 | advanced.setObserver(new CoapExchangeObserverProxy((ExchangeObserver) observerField.get(advanced), |
158 | 165 | registerAsyncCoapSession(exchange, request, sessionInfo, sessionId))); |
... | ... | @@ -319,7 +326,7 @@ public class CoapTransportResource extends CoapResource { |
319 | 326 | |
320 | 327 | @Override |
321 | 328 | public void onSuccess(Void msg) { |
322 | - exchange.respond(ResponseCode.VALID); | |
329 | + exchange.respond(ResponseCode.VALID); | |
323 | 330 | } |
324 | 331 | |
325 | 332 | @Override | ... | ... |
... | ... | @@ -22,7 +22,6 @@ import org.thingsboard.server.gen.transport.TransportProtos; |
22 | 22 | import org.thingsboard.server.transport.coap.CoapTransportResource; |
23 | 23 | |
24 | 24 | import java.util.UUID; |
25 | -import java.util.Optional; | |
26 | 25 | |
27 | 26 | public interface CoapTransportAdaptor { |
28 | 27 | |
... | ... | @@ -36,6 +35,8 @@ public interface CoapTransportAdaptor { |
36 | 35 | |
37 | 36 | TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(UUID sessionId, Request inbound) throws AdaptorException; |
38 | 37 | |
38 | + TransportProtos.ClaimDeviceMsg convertToClaimDevice(UUID sessionId, Request inbound, TransportProtos.SessionInfoProto sessionInfo) throws AdaptorException; | |
39 | + | |
39 | 40 | Response convertToPublish(CoapTransportResource.CoapSessionListener session, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException; |
40 | 41 | |
41 | 42 | Response convertToPublish(CoapTransportResource.CoapSessionListener session, TransportProtos.AttributeUpdateNotificationMsg notificationMsg) throws AdaptorException; | ... | ... |
... | ... | @@ -15,33 +15,36 @@ |
15 | 15 | */ |
16 | 16 | package org.thingsboard.server.transport.coap.adaptors; |
17 | 17 | |
18 | -import java.util.*; | |
19 | - | |
20 | 18 | import com.google.gson.JsonElement; |
21 | 19 | import com.google.gson.JsonObject; |
20 | +import com.google.gson.JsonParser; | |
21 | +import com.google.gson.JsonSyntaxException; | |
22 | 22 | import lombok.extern.slf4j.Slf4j; |
23 | 23 | import org.eclipse.californium.core.coap.CoAP; |
24 | 24 | import org.eclipse.californium.core.coap.Request; |
25 | 25 | import org.eclipse.californium.core.coap.Response; |
26 | +import org.springframework.stereotype.Component; | |
26 | 27 | import org.springframework.util.StringUtils; |
27 | -import org.thingsboard.server.common.msg.kv.AttributesKVMsg; | |
28 | -import org.thingsboard.server.common.msg.session.SessionContext; | |
28 | +import org.thingsboard.server.common.data.id.DeviceId; | |
29 | 29 | import org.thingsboard.server.common.transport.adaptor.AdaptorException; |
30 | 30 | import org.thingsboard.server.common.transport.adaptor.JsonConverter; |
31 | -import org.springframework.stereotype.Component; | |
32 | - | |
33 | -import com.google.gson.JsonParser; | |
34 | -import com.google.gson.JsonSyntaxException; | |
35 | 31 | import org.thingsboard.server.gen.transport.TransportProtos; |
36 | 32 | import org.thingsboard.server.transport.coap.CoapTransportResource; |
37 | 33 | |
34 | +import java.util.Arrays; | |
35 | +import java.util.HashSet; | |
36 | +import java.util.List; | |
37 | +import java.util.Optional; | |
38 | +import java.util.Set; | |
39 | +import java.util.UUID; | |
40 | + | |
38 | 41 | @Component("JsonCoapAdaptor") |
39 | 42 | @Slf4j |
40 | 43 | public class JsonCoapAdaptor implements CoapTransportAdaptor { |
41 | 44 | |
42 | 45 | @Override |
43 | 46 | public TransportProtos.PostTelemetryMsg convertToPostTelemetry(UUID sessionId, Request inbound) throws AdaptorException { |
44 | - String payload = validatePayload(sessionId, inbound); | |
47 | + String payload = validatePayload(sessionId, inbound, false); | |
45 | 48 | try { |
46 | 49 | return JsonConverter.convertToTelemetryProto(new JsonParser().parse(payload)); |
47 | 50 | } catch (IllegalStateException | JsonSyntaxException ex) { |
... | ... | @@ -51,7 +54,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor { |
51 | 54 | |
52 | 55 | @Override |
53 | 56 | public TransportProtos.PostAttributeMsg convertToPostAttributes(UUID sessionId, Request inbound) throws AdaptorException { |
54 | - String payload = validatePayload(sessionId, inbound); | |
57 | + String payload = validatePayload(sessionId, inbound, false); | |
55 | 58 | try { |
56 | 59 | return JsonConverter.convertToAttributesProto(new JsonParser().parse(payload)); |
57 | 60 | } catch (IllegalStateException | JsonSyntaxException ex) { |
... | ... | @@ -79,7 +82,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor { |
79 | 82 | @Override |
80 | 83 | public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(UUID sessionId, Request inbound) throws AdaptorException { |
81 | 84 | Optional<Integer> requestId = CoapTransportResource.getRequestId(inbound); |
82 | - String payload = validatePayload(sessionId, inbound); | |
85 | + String payload = validatePayload(sessionId, inbound, false); | |
83 | 86 | JsonObject response = new JsonParser().parse(payload).getAsJsonObject(); |
84 | 87 | return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId.orElseThrow(() -> new AdaptorException("Request id is missing!"))) |
85 | 88 | .setPayload(response.toString()).build(); |
... | ... | @@ -87,11 +90,22 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor { |
87 | 90 | |
88 | 91 | @Override |
89 | 92 | public TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(UUID sessionId, Request inbound) throws AdaptorException { |
90 | - String payload = validatePayload(sessionId, inbound); | |
93 | + String payload = validatePayload(sessionId, inbound, false); | |
91 | 94 | return JsonConverter.convertToServerRpcRequest(new JsonParser().parse(payload), 0); |
92 | 95 | } |
93 | 96 | |
94 | 97 | @Override |
98 | + public TransportProtos.ClaimDeviceMsg convertToClaimDevice(UUID sessionId, Request inbound, TransportProtos.SessionInfoProto sessionInfo) throws AdaptorException { | |
99 | + DeviceId deviceId = new DeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB())); | |
100 | + String payload = validatePayload(sessionId, inbound, true); | |
101 | + try { | |
102 | + return JsonConverter.convertToClaimDeviceProto(deviceId, payload); | |
103 | + } catch (IllegalStateException | JsonSyntaxException ex) { | |
104 | + throw new AdaptorException(ex); | |
105 | + } | |
106 | + } | |
107 | + | |
108 | + @Override | |
95 | 109 | public Response convertToPublish(CoapTransportResource.CoapSessionListener session, TransportProtos.AttributeUpdateNotificationMsg msg) throws AdaptorException { |
96 | 110 | return getObserveNotification(session.getNextSeqNumber(), JsonConverter.toJson(msg)); |
97 | 111 | } |
... | ... | @@ -128,11 +142,13 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor { |
128 | 142 | return response; |
129 | 143 | } |
130 | 144 | |
131 | - private String validatePayload(UUID sessionId, Request inbound) throws AdaptorException { | |
145 | + private String validatePayload(UUID sessionId, Request inbound, boolean isEmptyPayloadAllowed) throws AdaptorException { | |
132 | 146 | String payload = inbound.getPayloadString(); |
133 | 147 | if (payload == null) { |
134 | 148 | log.warn("[{}] Payload is empty!", sessionId); |
135 | - throw new AdaptorException(new IllegalArgumentException("Payload is empty!")); | |
149 | + if (!isEmptyPayloadAllowed) { | |
150 | + throw new AdaptorException(new IllegalArgumentException("Payload is empty!")); | |
151 | + } | |
136 | 152 | } |
137 | 153 | return payload; |
138 | 154 | } | ... | ... |
... | ... | @@ -20,7 +20,6 @@ import com.google.gson.JsonParser; |
20 | 20 | import lombok.extern.slf4j.Slf4j; |
21 | 21 | import org.springframework.beans.factory.annotation.Autowired; |
22 | 22 | import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; |
23 | -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | |
24 | 23 | import org.springframework.http.HttpStatus; |
25 | 24 | import org.springframework.http.ResponseEntity; |
26 | 25 | import org.springframework.util.StringUtils; |
... | ... | @@ -31,6 +30,7 @@ import org.springframework.web.bind.annotation.RequestMethod; |
31 | 30 | import org.springframework.web.bind.annotation.RequestParam; |
32 | 31 | import org.springframework.web.bind.annotation.RestController; |
33 | 32 | import org.springframework.web.context.request.async.DeferredResult; |
33 | +import org.thingsboard.server.common.data.id.DeviceId; | |
34 | 34 | import org.thingsboard.server.common.transport.SessionMsgListener; |
35 | 35 | import org.thingsboard.server.common.transport.TransportContext; |
36 | 36 | import org.thingsboard.server.common.transport.TransportService; |
... | ... | @@ -119,6 +119,20 @@ public class DeviceApiController { |
119 | 119 | return responseWriter; |
120 | 120 | } |
121 | 121 | |
122 | + @RequestMapping(value = "/{deviceToken}/claim", method = RequestMethod.POST) | |
123 | + public DeferredResult<ResponseEntity> claimDevice(@PathVariable("deviceToken") String deviceToken, | |
124 | + @RequestBody(required = false) String json, HttpServletRequest request) { | |
125 | + DeferredResult<ResponseEntity> responseWriter = new DeferredResult<>(); | |
126 | + transportContext.getTransportService().process(ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), | |
127 | + new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { | |
128 | + TransportService transportService = transportContext.getTransportService(); | |
129 | + DeviceId deviceId = new DeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB())); | |
130 | + transportService.process(sessionInfo, JsonConverter.convertToClaimDeviceProto(deviceId, json), | |
131 | + new HttpOkCallback(responseWriter)); | |
132 | + })); | |
133 | + return responseWriter; | |
134 | + } | |
135 | + | |
122 | 136 | @RequestMapping(value = "/{deviceToken}/rpc", method = RequestMethod.GET, produces = "application/json") |
123 | 137 | public DeferredResult<ResponseEntity> subscribeToCommands(@PathVariable("deviceToken") String deviceToken, |
124 | 138 | @RequestParam(value = "timeout", required = false, defaultValue = "0") long timeout, | ... | ... |
... | ... | @@ -29,6 +29,7 @@ public class MqttTopics { |
29 | 29 | public static final String DEVICE_ATTRIBUTES_RESPONSES_TOPIC = DEVICE_ATTRIBUTES_RESPONSE_TOPIC_PREFIX + "+"; |
30 | 30 | public static final String DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX = BASE_DEVICE_API_TOPIC + "/attributes/request/"; |
31 | 31 | public static final String DEVICE_TELEMETRY_TOPIC = BASE_DEVICE_API_TOPIC + "/telemetry"; |
32 | + public static final String DEVICE_CLAIM_TOPIC = BASE_DEVICE_API_TOPIC + "/claim"; | |
32 | 33 | public static final String DEVICE_ATTRIBUTES_TOPIC = BASE_DEVICE_API_TOPIC + "/attributes"; |
33 | 34 | |
34 | 35 | public static final String BASE_GATEWAY_API_TOPIC = "v1/gateway"; |
... | ... | @@ -36,6 +37,7 @@ public class MqttTopics { |
36 | 37 | public static final String GATEWAY_DISCONNECT_TOPIC = BASE_GATEWAY_API_TOPIC + "/disconnect"; |
37 | 38 | public static final String GATEWAY_ATTRIBUTES_TOPIC = BASE_GATEWAY_API_TOPIC + "/attributes"; |
38 | 39 | public static final String GATEWAY_TELEMETRY_TOPIC = BASE_GATEWAY_API_TOPIC + "/telemetry"; |
40 | + public static final String GATEWAY_CLAIM_TOPIC = BASE_GATEWAY_API_TOPIC + "/claim"; | |
39 | 41 | public static final String GATEWAY_RPC_TOPIC = BASE_GATEWAY_API_TOPIC + "/rpc"; |
40 | 42 | public static final String GATEWAY_ATTRIBUTES_REQUEST_TOPIC = BASE_GATEWAY_API_TOPIC + "/attributes/request"; |
41 | 43 | public static final String GATEWAY_ATTRIBUTES_RESPONSE_TOPIC = BASE_GATEWAY_API_TOPIC + "/attributes/response"; | ... | ... |
... | ... | @@ -38,11 +38,11 @@ import io.netty.util.concurrent.Future; |
38 | 38 | import io.netty.util.concurrent.GenericFutureListener; |
39 | 39 | import lombok.extern.slf4j.Slf4j; |
40 | 40 | import org.springframework.util.StringUtils; |
41 | +import org.thingsboard.server.common.msg.EncryptionUtil; | |
41 | 42 | import org.thingsboard.server.common.transport.SessionMsgListener; |
42 | 43 | import org.thingsboard.server.common.transport.TransportService; |
43 | 44 | import org.thingsboard.server.common.transport.TransportServiceCallback; |
44 | 45 | import org.thingsboard.server.common.transport.adaptor.AdaptorException; |
45 | -import org.thingsboard.server.common.msg.EncryptionUtil; | |
46 | 46 | import org.thingsboard.server.common.transport.service.AbstractTransportService; |
47 | 47 | import org.thingsboard.server.gen.transport.TransportProtos; |
48 | 48 | import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto; |
... | ... | @@ -183,6 +183,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement |
183 | 183 | case MqttTopics.GATEWAY_TELEMETRY_TOPIC: |
184 | 184 | gatewaySessionHandler.onDeviceTelemetry(mqttMsg); |
185 | 185 | break; |
186 | + case MqttTopics.GATEWAY_CLAIM_TOPIC: | |
187 | + gatewaySessionHandler.onDeviceClaim(mqttMsg); | |
188 | + break; | |
186 | 189 | case MqttTopics.GATEWAY_ATTRIBUTES_TOPIC: |
187 | 190 | gatewaySessionHandler.onDeviceAttributes(mqttMsg); |
188 | 191 | break; |
... | ... | @@ -221,6 +224,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement |
221 | 224 | } else if (topicName.startsWith(MqttTopics.DEVICE_RPC_REQUESTS_TOPIC)) { |
222 | 225 | TransportProtos.ToServerRpcRequestMsg rpcRequestMsg = adaptor.convertToServerRpcRequest(deviceSessionCtx, mqttMsg); |
223 | 226 | transportService.process(sessionInfo, rpcRequestMsg, getPubAckCallback(ctx, msgId, rpcRequestMsg)); |
227 | + } else if (topicName.equals(MqttTopics.DEVICE_CLAIM_TOPIC)) { | |
228 | + TransportProtos.ClaimDeviceMsg claimDeviceMsg = adaptor.convertToClaimDevice(deviceSessionCtx, mqttMsg); | |
229 | + transportService.process(sessionInfo, claimDeviceMsg, getPubAckCallback(ctx, msgId, claimDeviceMsg)); | |
224 | 230 | } |
225 | 231 | } catch (AdaptorException e) { |
226 | 232 | log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); | ... | ... |
... | ... | @@ -57,7 +57,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { |
57 | 57 | |
58 | 58 | @Override |
59 | 59 | public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { |
60 | - String payload = validatePayload(ctx.getSessionId(), inbound.payload()); | |
60 | + String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false); | |
61 | 61 | try { |
62 | 62 | return JsonConverter.convertToTelemetryProto(new JsonParser().parse(payload)); |
63 | 63 | } catch (IllegalStateException | JsonSyntaxException ex) { |
... | ... | @@ -67,7 +67,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { |
67 | 67 | |
68 | 68 | @Override |
69 | 69 | public TransportProtos.PostAttributeMsg convertToPostAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { |
70 | - String payload = validatePayload(ctx.getSessionId(), inbound.payload()); | |
70 | + String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false); | |
71 | 71 | try { |
72 | 72 | return JsonConverter.convertToAttributesProto(new JsonParser().parse(payload)); |
73 | 73 | } catch (IllegalStateException | JsonSyntaxException ex) { |
... | ... | @@ -114,7 +114,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { |
114 | 114 | @Override |
115 | 115 | public TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { |
116 | 116 | String topicName = inbound.variableHeader().topicName(); |
117 | - String payload = validatePayload(ctx.getSessionId(), inbound.payload()); | |
117 | + String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false); | |
118 | 118 | try { |
119 | 119 | Integer requestId = Integer.valueOf(topicName.substring(MqttTopics.DEVICE_RPC_REQUESTS_TOPIC.length())); |
120 | 120 | return JsonConverter.convertToServerRpcRequest(new JsonParser().parse(payload), requestId); |
... | ... | @@ -124,6 +124,16 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { |
124 | 124 | } |
125 | 125 | |
126 | 126 | @Override |
127 | + public TransportProtos.ClaimDeviceMsg convertToClaimDevice(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { | |
128 | + String payload = validatePayload(ctx.getSessionId(), inbound.payload(), true); | |
129 | + try { | |
130 | + return JsonConverter.convertToClaimDeviceProto(ctx.getDeviceId(), payload); | |
131 | + } catch (IllegalStateException | JsonSyntaxException ex) { | |
132 | + throw new AdaptorException(ex); | |
133 | + } | |
134 | + } | |
135 | + | |
136 | + @Override | |
127 | 137 | public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { |
128 | 138 | if (!StringUtils.isEmpty(responseMsg.getError())) { |
129 | 139 | throw new AdaptorException(responseMsg.getError()); |
... | ... | @@ -193,7 +203,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { |
193 | 203 | } |
194 | 204 | |
195 | 205 | public static JsonElement validateJsonPayload(UUID sessionId, ByteBuf payloadData) throws AdaptorException { |
196 | - String payload = validatePayload(sessionId, payloadData); | |
206 | + String payload = validatePayload(sessionId, payloadData, false); | |
197 | 207 | try { |
198 | 208 | return new JsonParser().parse(payload); |
199 | 209 | } catch (JsonSyntaxException ex) { |
... | ... | @@ -201,12 +211,14 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { |
201 | 211 | } |
202 | 212 | } |
203 | 213 | |
204 | - private static String validatePayload(UUID sessionId, ByteBuf payloadData) throws AdaptorException { | |
214 | + private static String validatePayload(UUID sessionId, ByteBuf payloadData, boolean isEmptyPayloadAllowed) throws AdaptorException { | |
205 | 215 | try { |
206 | 216 | String payload = payloadData.toString(UTF8); |
207 | 217 | if (payload == null) { |
208 | 218 | log.warn("[{}] Payload is empty!", sessionId); |
209 | - throw new AdaptorException(new IllegalArgumentException("Payload is empty!")); | |
219 | + if (!isEmptyPayloadAllowed) { | |
220 | + throw new AdaptorException(new IllegalArgumentException("Payload is empty!")); | |
221 | + } | |
210 | 222 | } |
211 | 223 | return payload; |
212 | 224 | } finally { | ... | ... |
... | ... | @@ -19,6 +19,7 @@ import io.netty.handler.codec.mqtt.MqttMessage; |
19 | 19 | import io.netty.handler.codec.mqtt.MqttPublishMessage; |
20 | 20 | import org.thingsboard.server.common.transport.adaptor.AdaptorException; |
21 | 21 | import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; |
22 | +import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg; | |
22 | 23 | import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg; |
23 | 24 | import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg; |
24 | 25 | import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg; |
... | ... | @@ -46,6 +47,8 @@ public interface MqttTransportAdaptor { |
46 | 47 | |
47 | 48 | ToServerRpcRequestMsg convertToServerRpcRequest(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg) throws AdaptorException; |
48 | 49 | |
50 | + ClaimDeviceMsg convertToClaimDevice(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException; | |
51 | + | |
49 | 52 | Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, GetAttributeResponseMsg responseMsg) throws AdaptorException; |
50 | 53 | |
51 | 54 | Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, GetAttributeResponseMsg responseMsg) throws AdaptorException; |
... | ... | @@ -59,4 +62,5 @@ public interface MqttTransportAdaptor { |
59 | 62 | Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, ToDeviceRpcRequestMsg rpcRequest) throws AdaptorException; |
60 | 63 | |
61 | 64 | Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, ToServerRpcResponseMsg rpcResponse) throws AdaptorException; |
65 | + | |
62 | 66 | } | ... | ... |
... | ... | @@ -30,6 +30,7 @@ import io.netty.handler.codec.mqtt.MqttMessage; |
30 | 30 | import io.netty.handler.codec.mqtt.MqttPublishMessage; |
31 | 31 | import lombok.extern.slf4j.Slf4j; |
32 | 32 | import org.springframework.util.StringUtils; |
33 | +import org.thingsboard.server.common.data.id.DeviceId; | |
33 | 34 | import org.thingsboard.server.common.transport.TransportService; |
34 | 35 | import org.thingsboard.server.common.transport.TransportServiceCallback; |
35 | 36 | import org.thingsboard.server.common.transport.adaptor.AdaptorException; |
... | ... | @@ -183,7 +184,42 @@ public class GatewaySessionHandler { |
183 | 184 | |
184 | 185 | @Override |
185 | 186 | public void onFailure(Throwable t) { |
186 | - log.debug("[{}] Failed to process device teleemtry command: {}", sessionId, deviceName, t); | |
187 | + log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t); | |
188 | + } | |
189 | + }, context.getExecutor()); | |
190 | + } | |
191 | + } else { | |
192 | + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); | |
193 | + } | |
194 | + } | |
195 | + | |
196 | + public void onDeviceClaim(MqttPublishMessage mqttMsg) throws AdaptorException { | |
197 | + JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, mqttMsg.payload()); | |
198 | + int msgId = mqttMsg.variableHeader().packetId(); | |
199 | + if (json.isJsonObject()) { | |
200 | + JsonObject jsonObj = json.getAsJsonObject(); | |
201 | + for (Map.Entry<String, JsonElement> deviceEntry : jsonObj.entrySet()) { | |
202 | + String deviceName = deviceEntry.getKey(); | |
203 | + Futures.addCallback(checkDeviceConnected(deviceName), | |
204 | + new FutureCallback<GatewayDeviceSessionCtx>() { | |
205 | + @Override | |
206 | + public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { | |
207 | + if (!deviceEntry.getValue().isJsonObject()) { | |
208 | + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); | |
209 | + } | |
210 | + try { | |
211 | + DeviceId deviceId = deviceCtx.getDeviceId(); | |
212 | + TransportProtos.ClaimDeviceMsg claimDeviceMsg = JsonConverter.convertToClaimDeviceProto(deviceId, deviceEntry.getValue()); | |
213 | + transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(channel, deviceName, msgId, claimDeviceMsg)); | |
214 | + } catch (Throwable e) { | |
215 | + UUID gatewayId = new UUID(gateway.getDeviceIdMSB(), gateway.getDeviceIdLSB()); | |
216 | + log.warn("[{}][{}] Failed to convert claim message: {}", gatewayId, deviceName, deviceEntry.getValue(), e); | |
217 | + } | |
218 | + } | |
219 | + | |
220 | + @Override | |
221 | + public void onFailure(Throwable t) { | |
222 | + log.debug("[{}] Failed to process device claiming command: {}", sessionId, deviceName, t); | |
187 | 223 | } |
188 | 224 | }, context.getExecutor()); |
189 | 225 | } |
... | ... | @@ -209,6 +245,7 @@ public class GatewaySessionHandler { |
209 | 245 | TransportProtos.PostAttributeMsg postAttributeMsg = JsonConverter.convertToAttributesProto(deviceEntry.getValue().getAsJsonObject()); |
210 | 246 | transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg)); |
211 | 247 | } |
248 | + | |
212 | 249 | @Override |
213 | 250 | public void onFailure(Throwable t) { |
214 | 251 | log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t); | ... | ... |
... | ... | @@ -16,18 +16,19 @@ |
16 | 16 | package org.thingsboard.server.common.transport; |
17 | 17 | |
18 | 18 | import org.thingsboard.server.gen.transport.TransportProtos; |
19 | -import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcRequestMsg; | |
20 | -import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseMsg; | |
21 | -import org.thingsboard.server.gen.transport.TransportProtos.SubscribeToAttributeUpdatesMsg; | |
22 | -import org.thingsboard.server.gen.transport.TransportProtos.SubscribeToRPCMsg; | |
23 | -import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; | |
19 | +import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg; | |
20 | +import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg; | |
24 | 21 | import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg; |
25 | 22 | import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg; |
26 | 23 | import org.thingsboard.server.gen.transport.TransportProtos.SessionEventMsg; |
24 | +import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; | |
25 | +import org.thingsboard.server.gen.transport.TransportProtos.SubscribeToAttributeUpdatesMsg; | |
26 | +import org.thingsboard.server.gen.transport.TransportProtos.SubscribeToRPCMsg; | |
27 | +import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseMsg; | |
28 | +import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcRequestMsg; | |
27 | 29 | import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; |
28 | 30 | import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; |
29 | 31 | import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; |
30 | -import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg; | |
31 | 32 | |
32 | 33 | /** |
33 | 34 | * Created by ashvayka on 04.10.18. |
... | ... | @@ -63,6 +64,8 @@ public interface TransportService { |
63 | 64 | |
64 | 65 | void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.SubscriptionInfoProto msg, TransportServiceCallback<Void> callback); |
65 | 66 | |
67 | + void process(SessionInfoProto sessionInfo, ClaimDeviceMsg msg, TransportServiceCallback<Void> callback); | |
68 | + | |
66 | 69 | void registerAsyncSession(SessionInfoProto sessionInfo, SessionMsgListener listener); |
67 | 70 | |
68 | 71 | void registerSyncSession(SessionInfoProto sessionInfo, SessionMsgListener listener, long timeout); | ... | ... |
... | ... | @@ -24,6 +24,8 @@ import com.google.gson.JsonPrimitive; |
24 | 24 | import com.google.gson.JsonSyntaxException; |
25 | 25 | import org.apache.commons.lang3.math.NumberUtils; |
26 | 26 | import org.springframework.util.StringUtils; |
27 | +import org.thingsboard.server.common.data.DataConstants; | |
28 | +import org.thingsboard.server.common.data.id.DeviceId; | |
27 | 29 | import org.thingsboard.server.common.data.kv.AttributeKey; |
28 | 30 | import org.thingsboard.server.common.data.kv.AttributeKvEntry; |
29 | 31 | import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; |
... | ... | @@ -35,6 +37,7 @@ import org.thingsboard.server.common.data.kv.StringDataEntry; |
35 | 37 | import org.thingsboard.server.common.msg.kv.AttributesKVMsg; |
36 | 38 | import org.thingsboard.server.gen.transport.TransportProtos; |
37 | 39 | import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; |
40 | +import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg; | |
38 | 41 | import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg; |
39 | 42 | import org.thingsboard.server.gen.transport.TransportProtos.KeyValueProto; |
40 | 43 | import org.thingsboard.server.gen.transport.TransportProtos.KeyValueType; |
... | ... | @@ -63,23 +66,69 @@ public class JsonConverter { |
63 | 66 | |
64 | 67 | private static int maxStringValueLength = 0; |
65 | 68 | |
66 | - public static PostTelemetryMsg convertToTelemetryProto(JsonElement jsonObject) throws JsonSyntaxException { | |
67 | - long systemTs = System.currentTimeMillis(); | |
69 | + public static PostTelemetryMsg convertToTelemetryProto(JsonElement jsonElement) throws JsonSyntaxException { | |
68 | 70 | PostTelemetryMsg.Builder builder = PostTelemetryMsg.newBuilder(); |
69 | - if (jsonObject.isJsonObject()) { | |
70 | - parseObject(builder, systemTs, jsonObject); | |
71 | - } else if (jsonObject.isJsonArray()) { | |
72 | - jsonObject.getAsJsonArray().forEach(je -> { | |
71 | + convertToTelemetry(jsonElement, System.currentTimeMillis(), null, builder); | |
72 | + return builder.build(); | |
73 | + } | |
74 | + | |
75 | + private static void convertToTelemetry(JsonElement jsonElement, long systemTs, Map<Long, List<KvEntry>> result, PostTelemetryMsg.Builder builder) { | |
76 | + if (jsonElement.isJsonObject()) { | |
77 | + parseObject(systemTs, result, builder, jsonElement.getAsJsonObject()); | |
78 | + } else if (jsonElement.isJsonArray()) { | |
79 | + jsonElement.getAsJsonArray().forEach(je -> { | |
73 | 80 | if (je.isJsonObject()) { |
74 | - parseObject(builder, systemTs, je.getAsJsonObject()); | |
81 | + parseObject(systemTs, result, builder, je.getAsJsonObject()); | |
75 | 82 | } else { |
76 | 83 | throw new JsonSyntaxException(CAN_T_PARSE_VALUE + je); |
77 | 84 | } |
78 | 85 | }); |
79 | 86 | } else { |
80 | - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + jsonObject); | |
87 | + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + jsonElement); | |
81 | 88 | } |
82 | - return builder.build(); | |
89 | + } | |
90 | + | |
91 | + private static void parseObject(long systemTs, Map<Long, List<KvEntry>> result, PostTelemetryMsg.Builder builder, JsonObject jo) { | |
92 | + if (result != null) { | |
93 | + parseObject(result, systemTs, jo); | |
94 | + } else { | |
95 | + parseObject(builder, systemTs, jo); | |
96 | + } | |
97 | + } | |
98 | + | |
99 | + public static ClaimDeviceMsg convertToClaimDeviceProto(DeviceId deviceId, String json) { | |
100 | + long durationMs = 0L; | |
101 | + if (json != null && !json.isEmpty()) { | |
102 | + return convertToClaimDeviceProto(deviceId, new JsonParser().parse(json)); | |
103 | + } | |
104 | + return buildClaimDeviceMsg(deviceId, DataConstants.DEFAULT_SECRET_KEY, durationMs); | |
105 | + } | |
106 | + | |
107 | + public static ClaimDeviceMsg convertToClaimDeviceProto(DeviceId deviceId, JsonElement jsonElement) { | |
108 | + String secretKey = DataConstants.DEFAULT_SECRET_KEY; | |
109 | + long durationMs = 0L; | |
110 | + if (jsonElement.isJsonObject()) { | |
111 | + JsonObject jo = jsonElement.getAsJsonObject(); | |
112 | + if (jo.has(DataConstants.SECRET_KEY_FIELD_NAME)) { | |
113 | + secretKey = jo.get(DataConstants.SECRET_KEY_FIELD_NAME).getAsString(); | |
114 | + } | |
115 | + if (jo.has(DataConstants.DURATION_MS_FIELD_NAME)) { | |
116 | + durationMs = jo.get(DataConstants.DURATION_MS_FIELD_NAME).getAsLong(); | |
117 | + } | |
118 | + } else { | |
119 | + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + jsonElement); | |
120 | + } | |
121 | + return buildClaimDeviceMsg(deviceId, secretKey, durationMs); | |
122 | + } | |
123 | + | |
124 | + private static ClaimDeviceMsg buildClaimDeviceMsg(DeviceId deviceId, String secretKey, long durationMs) { | |
125 | + ClaimDeviceMsg.Builder result = ClaimDeviceMsg.newBuilder(); | |
126 | + return result | |
127 | + .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) | |
128 | + .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) | |
129 | + .setSecretKey(secretKey) | |
130 | + .setDurationMs(durationMs) | |
131 | + .build(); | |
83 | 132 | } |
84 | 133 | |
85 | 134 | public static PostAttributeMsg convertToAttributesProto(JsonElement jsonObject) throws JsonSyntaxException { |
... | ... | @@ -103,8 +152,7 @@ public class JsonConverter { |
103 | 152 | return result; |
104 | 153 | } |
105 | 154 | |
106 | - private static void parseObject(PostTelemetryMsg.Builder builder, long systemTs, JsonElement jsonObject) { | |
107 | - JsonObject jo = jsonObject.getAsJsonObject(); | |
155 | + private static void parseObject(PostTelemetryMsg.Builder builder, long systemTs, JsonObject jo) { | |
108 | 156 | if (jo.has("ts") && jo.has("values")) { |
109 | 157 | parseWithTs(builder, jo); |
110 | 158 | } else { |
... | ... | @@ -137,7 +185,7 @@ public class JsonConverter { |
137 | 185 | String message = String.format("String value length [%d] for key [%s] is greater than maximum allowed [%d]", value.getAsString().length(), valueEntry.getKey(), maxStringValueLength); |
138 | 186 | throw new JsonSyntaxException(message); |
139 | 187 | } |
140 | - if(isTypeCastEnabled && NumberUtils.isParsable(value.getAsString())) { | |
188 | + if (isTypeCastEnabled && NumberUtils.isParsable(value.getAsString())) { | |
141 | 189 | try { |
142 | 190 | result.add(buildNumericKeyValueProto(value, valueEntry.getKey())); |
143 | 191 | } catch (RuntimeException th) { |
... | ... | @@ -400,7 +448,7 @@ public class JsonConverter { |
400 | 448 | String message = String.format("String value length [%d] for key [%s] is greater than maximum allowed [%d]", value.getAsString().length(), valueEntry.getKey(), maxStringValueLength); |
401 | 449 | throw new JsonSyntaxException(message); |
402 | 450 | } |
403 | - if(isTypeCastEnabled && NumberUtils.isParsable(value.getAsString())) { | |
451 | + if (isTypeCastEnabled && NumberUtils.isParsable(value.getAsString())) { | |
404 | 452 | try { |
405 | 453 | parseNumericValue(result, valueEntry, value); |
406 | 454 | } catch (RuntimeException th) { |
... | ... | @@ -423,26 +471,13 @@ public class JsonConverter { |
423 | 471 | return result; |
424 | 472 | } |
425 | 473 | |
426 | - public static Map<Long, List<KvEntry>> convertToTelemetry(JsonElement jsonObject, long systemTs) throws JsonSyntaxException { | |
474 | + public static Map<Long, List<KvEntry>> convertToTelemetry(JsonElement jsonElement, long systemTs) throws JsonSyntaxException { | |
427 | 475 | Map<Long, List<KvEntry>> result = new HashMap<>(); |
428 | - if (jsonObject.isJsonObject()) { | |
429 | - parseObject(result, systemTs, jsonObject); | |
430 | - } else if (jsonObject.isJsonArray()) { | |
431 | - jsonObject.getAsJsonArray().forEach(je -> { | |
432 | - if (je.isJsonObject()) { | |
433 | - parseObject(result, systemTs, je.getAsJsonObject()); | |
434 | - } else { | |
435 | - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + je); | |
436 | - } | |
437 | - }); | |
438 | - } else { | |
439 | - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + jsonObject); | |
440 | - } | |
476 | + convertToTelemetry(jsonElement, systemTs, result, null); | |
441 | 477 | return result; |
442 | 478 | } |
443 | 479 | |
444 | - private static void parseObject(Map<Long, List<KvEntry>> result, long systemTs, JsonElement jsonObject) { | |
445 | - JsonObject jo = jsonObject.getAsJsonObject(); | |
480 | + private static void parseObject(Map<Long, List<KvEntry>> result, long systemTs, JsonObject jo) { | |
446 | 481 | if (jo.has("ts") && jo.has("values")) { |
447 | 482 | parseWithTs(result, jo); |
448 | 483 | } else { | ... | ... |
... | ... | @@ -28,7 +28,12 @@ import org.thingsboard.server.common.transport.TransportServiceCallback; |
28 | 28 | import org.thingsboard.server.gen.transport.TransportProtos; |
29 | 29 | |
30 | 30 | import java.util.UUID; |
31 | -import java.util.concurrent.*; | |
31 | +import java.util.concurrent.ConcurrentHashMap; | |
32 | +import java.util.concurrent.ConcurrentMap; | |
33 | +import java.util.concurrent.ExecutorService; | |
34 | +import java.util.concurrent.Executors; | |
35 | +import java.util.concurrent.ScheduledExecutorService; | |
36 | +import java.util.concurrent.TimeUnit; | |
32 | 37 | |
33 | 38 | /** |
34 | 39 | * Created by ashvayka on 17.10.18. |
... | ... | @@ -128,6 +133,12 @@ public abstract class AbstractTransportService implements TransportService { |
128 | 133 | } |
129 | 134 | |
130 | 135 | @Override |
136 | + public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.ClaimDeviceMsg msg, | |
137 | + TransportServiceCallback<Void> callback) { | |
138 | + registerClaimingInfo(sessionInfo, msg, callback); | |
139 | + } | |
140 | + | |
141 | + @Override | |
131 | 142 | public void reportActivity(TransportProtos.SessionInfoProto sessionInfo) { |
132 | 143 | reportActivityInternal(sessionInfo); |
133 | 144 | } |
... | ... | @@ -148,6 +159,8 @@ public abstract class AbstractTransportService implements TransportService { |
148 | 159 | |
149 | 160 | protected abstract void doProcess(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.ToServerRpcRequestMsg msg, TransportServiceCallback<Void> callback); |
150 | 161 | |
162 | + protected abstract void registerClaimingInfo(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.ClaimDeviceMsg msg, TransportServiceCallback<Void> callback); | |
163 | + | |
151 | 164 | private SessionMetaData reportActivityInternal(TransportProtos.SessionInfoProto sessionInfo) { |
152 | 165 | UUID sessionId = toId(sessionInfo); |
153 | 166 | SessionMetaData sessionMetaData = sessions.get(sessionId); | ... | ... |
... | ... | @@ -24,34 +24,40 @@ import org.apache.kafka.clients.producer.RecordMetadata; |
24 | 24 | import org.springframework.beans.factory.annotation.Autowired; |
25 | 25 | import org.springframework.beans.factory.annotation.Value; |
26 | 26 | import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; |
27 | -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | |
28 | 27 | import org.springframework.stereotype.Service; |
29 | -import org.thingsboard.server.common.transport.SessionMsgListener; | |
30 | -import org.thingsboard.server.common.transport.TransportService; | |
31 | 28 | import org.thingsboard.server.common.transport.TransportServiceCallback; |
32 | -import org.thingsboard.server.gen.transport.TransportProtos; | |
33 | -import org.thingsboard.server.gen.transport.TransportProtos.*; | |
29 | +import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg; | |
30 | +import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg; | |
34 | 31 | import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; |
35 | 32 | import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg; |
36 | 33 | import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg; |
37 | 34 | import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg; |
38 | 35 | import org.thingsboard.server.gen.transport.TransportProtos.SessionEventMsg; |
39 | 36 | import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; |
37 | +import org.thingsboard.server.gen.transport.TransportProtos.SubscribeToAttributeUpdatesMsg; | |
38 | +import org.thingsboard.server.gen.transport.TransportProtos.SubscribeToRPCMsg; | |
39 | +import org.thingsboard.server.gen.transport.TransportProtos.SubscriptionInfoProto; | |
40 | +import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseMsg; | |
41 | +import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; | |
42 | +import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcRequestMsg; | |
40 | 43 | import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; |
41 | 44 | import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; |
42 | 45 | import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; |
43 | -import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; | |
46 | +import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg; | |
44 | 47 | import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; |
45 | 48 | import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; |
46 | 49 | import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; |
47 | -import org.thingsboard.server.kafka.*; | |
50 | +import org.thingsboard.server.kafka.AsyncCallbackTemplate; | |
51 | +import org.thingsboard.server.kafka.TBKafkaAdmin; | |
52 | +import org.thingsboard.server.kafka.TBKafkaConsumerTemplate; | |
53 | +import org.thingsboard.server.kafka.TBKafkaProducerTemplate; | |
54 | +import org.thingsboard.server.kafka.TbKafkaRequestTemplate; | |
55 | +import org.thingsboard.server.kafka.TbKafkaSettings; | |
56 | +import org.thingsboard.server.kafka.TbNodeIdProvider; | |
48 | 57 | |
49 | 58 | import javax.annotation.PostConstruct; |
50 | 59 | import javax.annotation.PreDestroy; |
51 | 60 | import java.time.Duration; |
52 | -import java.util.UUID; | |
53 | -import java.util.concurrent.ConcurrentHashMap; | |
54 | -import java.util.concurrent.ConcurrentMap; | |
55 | 61 | import java.util.concurrent.ExecutorService; |
56 | 62 | import java.util.concurrent.Executors; |
57 | 63 | |
... | ... | @@ -305,6 +311,15 @@ public class RemoteTransportService extends AbstractTransportService { |
305 | 311 | send(sessionInfo, toRuleEngineMsg, callback); |
306 | 312 | } |
307 | 313 | |
314 | + @Override | |
315 | + protected void registerClaimingInfo(SessionInfoProto sessionInfo, ClaimDeviceMsg msg, TransportServiceCallback<Void> callback) { | |
316 | + ToRuleEngineMsg toRuleEngineMsg = ToRuleEngineMsg.newBuilder().setToDeviceActorMsg( | |
317 | + TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo) | |
318 | + .setClaimDevice(msg).build() | |
319 | + ).build(); | |
320 | + send(sessionInfo, toRuleEngineMsg, callback); | |
321 | + } | |
322 | + | |
308 | 323 | private static class TransportCallbackAdaptor implements Callback { |
309 | 324 | private final TransportServiceCallback<Void> callback; |
310 | 325 | ... | ... |
... | ... | @@ -172,6 +172,13 @@ message ToServerRpcResponseMsg { |
172 | 172 | string error = 3; |
173 | 173 | } |
174 | 174 | |
175 | +message ClaimDeviceMsg { | |
176 | + int64 deviceIdMSB = 1; | |
177 | + int64 deviceIdLSB = 2; | |
178 | + string secretKey = 3; | |
179 | + int64 durationMs = 4; | |
180 | +} | |
181 | + | |
175 | 182 | //Used to report session state to tb-node and persist this state in the cache on the tb-node level. |
176 | 183 | message SubscriptionInfoProto { |
177 | 184 | int64 lastActivityTime = 1; |
... | ... | @@ -199,6 +206,7 @@ message TransportToDeviceActorMsg { |
199 | 206 | ToDeviceRpcResponseMsg toDeviceRPCCallResponse = 8; |
200 | 207 | ToServerRpcRequestMsg toServerRPCCallRequest = 9; |
201 | 208 | SubscriptionInfoProto subscriptionInfo = 10; |
209 | + ClaimDeviceMsg claimDevice = 11; | |
202 | 210 | } |
203 | 211 | |
204 | 212 | message DeviceActorToTransportMsg { | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.thingsboard.server.dao.device; | |
17 | + | |
18 | +import com.google.common.util.concurrent.ListenableFuture; | |
19 | +import org.thingsboard.server.common.data.Device; | |
20 | +import org.thingsboard.server.common.data.id.CustomerId; | |
21 | +import org.thingsboard.server.common.data.id.DeviceId; | |
22 | +import org.thingsboard.server.common.data.id.TenantId; | |
23 | +import org.thingsboard.server.dao.device.claim.ClaimResponse; | |
24 | + | |
25 | +import java.util.List; | |
26 | + | |
27 | +public interface ClaimDevicesService { | |
28 | + | |
29 | + ListenableFuture<Void> registerClaimingInfo(TenantId tenantId, DeviceId deviceId, String secretKey, long durationMs); | |
30 | + | |
31 | + ListenableFuture<ClaimResponse> claimDevice(Device device, CustomerId customerId, String secretKey); | |
32 | + | |
33 | + ListenableFuture<List<Void>> reClaimDevice(TenantId tenantId, Device device); | |
34 | + | |
35 | +} | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.thingsboard.server.dao.device; | |
17 | + | |
18 | +import com.google.common.util.concurrent.Futures; | |
19 | +import com.google.common.util.concurrent.ListenableFuture; | |
20 | +import lombok.extern.slf4j.Slf4j; | |
21 | +import org.springframework.beans.factory.annotation.Autowired; | |
22 | +import org.springframework.beans.factory.annotation.Value; | |
23 | +import org.springframework.cache.Cache; | |
24 | +import org.springframework.cache.CacheManager; | |
25 | +import org.springframework.stereotype.Service; | |
26 | +import org.thingsboard.server.common.data.DataConstants; | |
27 | +import org.thingsboard.server.common.data.Device; | |
28 | +import org.thingsboard.server.common.data.id.CustomerId; | |
29 | +import org.thingsboard.server.common.data.id.DeviceId; | |
30 | +import org.thingsboard.server.common.data.id.TenantId; | |
31 | +import org.thingsboard.server.common.data.kv.AttributeKvEntry; | |
32 | +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; | |
33 | +import org.thingsboard.server.common.data.kv.BooleanDataEntry; | |
34 | +import org.thingsboard.server.dao.attributes.AttributesService; | |
35 | +import org.thingsboard.server.dao.device.claim.ClaimData; | |
36 | +import org.thingsboard.server.dao.device.claim.ClaimResponse; | |
37 | +import org.thingsboard.server.dao.model.ModelConstants; | |
38 | + | |
39 | +import java.util.Collections; | |
40 | +import java.util.List; | |
41 | +import java.util.Optional; | |
42 | + | |
43 | +import static org.thingsboard.server.common.data.CacheConstants.CLAIM_DEVICES_CACHE; | |
44 | + | |
45 | +@Service | |
46 | +@Slf4j | |
47 | +public class ClaimDevicesServiceImpl implements ClaimDevicesService { | |
48 | + | |
49 | + private static final String CLAIM_ATTRIBUTE_NAME = "claimingAllowed"; | |
50 | + | |
51 | + @Autowired | |
52 | + private DeviceService deviceService; | |
53 | + @Autowired | |
54 | + private AttributesService attributesService; | |
55 | + @Autowired | |
56 | + private CacheManager cacheManager; | |
57 | + | |
58 | + @Value("${security.claim.allowClaimingByDefault}") | |
59 | + private boolean isAllowedClaimingByDefault; | |
60 | + | |
61 | + @Value("${security.claim.duration}") | |
62 | + private long systemDurationMs; | |
63 | + | |
64 | + @Override | |
65 | + public ListenableFuture<Void> registerClaimingInfo(TenantId tenantId, DeviceId deviceId, String secretKey, long durationMs) { | |
66 | + ListenableFuture<Device> deviceFuture = deviceService.findDeviceByIdAsync(tenantId, deviceId); | |
67 | + return Futures.transformAsync(deviceFuture, device -> { | |
68 | + Cache cache = cacheManager.getCache(CLAIM_DEVICES_CACHE); | |
69 | + List<Object> key = constructCacheKey(device.getId()); | |
70 | + | |
71 | + if (isAllowedClaimingByDefault) { | |
72 | + if (device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { | |
73 | + persistInCache(secretKey, durationMs, cache, key); | |
74 | + return Futures.immediateFuture(null); | |
75 | + } | |
76 | + log.warn("The device [{}] has been already claimed!", device.getName()); | |
77 | + throw new IllegalArgumentException(); | |
78 | + } else { | |
79 | + ListenableFuture<List<AttributeKvEntry>> claimingAllowedFuture = attributesService.find(tenantId, device.getId(), | |
80 | + DataConstants.SERVER_SCOPE, Collections.singletonList(CLAIM_ATTRIBUTE_NAME)); | |
81 | + return Futures.transform(claimingAllowedFuture, list -> { | |
82 | + if (list != null && !list.isEmpty()) { | |
83 | + Optional<Boolean> claimingAllowedOptional = list.get(0).getBooleanValue(); | |
84 | + if (claimingAllowedOptional.isPresent() && claimingAllowedOptional.get() | |
85 | + && device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { | |
86 | + persistInCache(secretKey, durationMs, cache, key); | |
87 | + return null; | |
88 | + } | |
89 | + } | |
90 | + log.warn("Failed to find claimingAllowed attribute for device or it is already claimed![{}]", device.getName()); | |
91 | + throw new IllegalArgumentException(); | |
92 | + }); | |
93 | + } | |
94 | + }); | |
95 | + } | |
96 | + | |
97 | + @Override | |
98 | + public ListenableFuture<ClaimResponse> claimDevice(Device device, CustomerId customerId, String secretKey) { | |
99 | + List<Object> key = constructCacheKey(device.getId()); | |
100 | + Cache cache = cacheManager.getCache(CLAIM_DEVICES_CACHE); | |
101 | + ClaimData claimData = cache.get(key, ClaimData.class); | |
102 | + if (claimData != null) { | |
103 | + long currTs = System.currentTimeMillis(); | |
104 | + if (currTs > claimData.getExpirationTime() || !secretKey.equals(claimData.getSecretKey())) { | |
105 | + log.warn("The claiming timeout occurred or wrong 'secretKey' provided for the device [{}]", device.getName()); | |
106 | + cache.evict(key); | |
107 | + return Futures.immediateFuture(ClaimResponse.FAILURE); | |
108 | + } else { | |
109 | + if (device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { | |
110 | + device.setCustomerId(customerId); | |
111 | + deviceService.saveDevice(device); | |
112 | + return Futures.transform(removeClaimingSavedData(cache, key, device), result -> ClaimResponse.SUCCESS); | |
113 | + } | |
114 | + return Futures.transform(removeClaimingSavedData(cache, key, device), result -> ClaimResponse.CLAIMED); | |
115 | + } | |
116 | + } else { | |
117 | + log.warn("Failed to find the device's claiming message![{}]", device.getName()); | |
118 | + return Futures.immediateFuture(ClaimResponse.CLAIMED); | |
119 | + } | |
120 | + } | |
121 | + | |
122 | + @Override | |
123 | + public ListenableFuture<List<Void>> reClaimDevice(TenantId tenantId, Device device) { | |
124 | + if (!device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { | |
125 | + cacheEviction(device.getId()); | |
126 | + | |
127 | + device.setCustomerId(null); | |
128 | + deviceService.saveDevice(device); | |
129 | + if (isAllowedClaimingByDefault) { | |
130 | + return Futures.immediateFuture(Collections.emptyList()); | |
131 | + } | |
132 | + return attributesService.save(tenantId, device.getId(), DataConstants.SERVER_SCOPE, Collections.singletonList( | |
133 | + new BaseAttributeKvEntry(new BooleanDataEntry(CLAIM_ATTRIBUTE_NAME, true), | |
134 | + System.currentTimeMillis()))); | |
135 | + } | |
136 | + cacheEviction(device.getId()); | |
137 | + return Futures.immediateFuture(Collections.emptyList()); | |
138 | + } | |
139 | + | |
140 | + private List<Object> constructCacheKey(DeviceId deviceId) { | |
141 | + return Collections.singletonList(deviceId); | |
142 | + } | |
143 | + | |
144 | + private void persistInCache(String secretKey, long durationMs, Cache cache, List<Object> key) { | |
145 | + ClaimData claimData = new ClaimData(secretKey, | |
146 | + System.currentTimeMillis() + validateDurationMs(durationMs)); | |
147 | + cache.putIfAbsent(key, claimData); | |
148 | + } | |
149 | + | |
150 | + private long validateDurationMs(long durationMs) { | |
151 | + if (durationMs > 0L) { | |
152 | + return durationMs; | |
153 | + } | |
154 | + return systemDurationMs; | |
155 | + } | |
156 | + | |
157 | + private ListenableFuture<List<Void>> removeClaimingSavedData(Cache cache, List<Object> key, Device device) { | |
158 | + cache.evict(key); | |
159 | + if (isAllowedClaimingByDefault) { | |
160 | + return Futures.immediateFuture(null); | |
161 | + } | |
162 | + return attributesService.removeAll(device.getTenantId(), | |
163 | + device.getId(), DataConstants.SERVER_SCOPE, Collections.singletonList(CLAIM_ATTRIBUTE_NAME)); | |
164 | + } | |
165 | + | |
166 | + private void cacheEviction(DeviceId deviceId) { | |
167 | + Cache cache = cacheManager.getCache(CLAIM_DEVICES_CACHE); | |
168 | + cache.evict(constructCacheKey(deviceId)); | |
169 | + } | |
170 | + | |
171 | +} | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.thingsboard.server.dao.device.claim; | |
17 | + | |
18 | +import lombok.AllArgsConstructor; | |
19 | +import lombok.Data; | |
20 | + | |
21 | +@AllArgsConstructor | |
22 | +@Data | |
23 | +public class ClaimData { | |
24 | + | |
25 | + private final String secretKey; | |
26 | + private final long expirationTime; | |
27 | + | |
28 | +} | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.thingsboard.server.dao.device.claim; | |
17 | + | |
18 | +public enum ClaimResponse { | |
19 | + | |
20 | + SUCCESS, | |
21 | + FAILURE, | |
22 | + CLAIMED | |
23 | + | |
24 | +} | ... | ... |
... | ... | @@ -27,11 +27,16 @@ caffeine.specs.assets.maxSize=100000 |
27 | 27 | caffeine.specs.entityViews.timeToLiveInMinutes=1440 |
28 | 28 | caffeine.specs.entityViews.maxSize=100000 |
29 | 29 | |
30 | +caffeine.specs.claimDevices.timeToLiveInMinutes=1440 | |
31 | +caffeine.specs.claimDevices.maxSize=100000 | |
32 | + | |
30 | 33 | redis.connection.host=localhost |
31 | 34 | redis.connection.port=6379 |
32 | 35 | redis.connection.db=0 |
33 | 36 | redis.connection.password= |
34 | 37 | |
35 | 38 | security.user_login_case_sensitive=true |
39 | +security.claim.allowClaimingByDefault=true | |
40 | +security.claim.duration=60000 | |
36 | 41 | |
37 | -database.ts_max_intervals=700 | |
\ No newline at end of file | ||
42 | +database.ts_max_intervals=700 | ... | ... |