Browse Source

Merge remote-tracking branch 'upstream/master' into fix/edge-compatibility-after-image-feature

pull/9804/head
Volodymyr Babak 3 years ago
parent
commit
ecc9094375
  1. 2
      application/src/main/data/json/system/widget_types/battery_level.json
  2. 6
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  3. 6
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  4. 24
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  5. 51
      application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
  6. 8
      application/src/main/java/org/thingsboard/server/controller/ImageController.java
  7. 320
      application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
  8. 33
      application/src/main/java/org/thingsboard/server/exception/ThingsboardCredentialsViolationResponse.java
  9. 4
      application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
  10. 7
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  11. 4
      application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
  12. 6
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java
  13. 9
      application/src/main/java/org/thingsboard/server/service/resource/DefaultTbImageService.java
  14. 3
      application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java
  15. 3
      application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java
  16. 16
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java
  17. 4
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java
  18. 7
      application/src/main/java/org/thingsboard/server/service/security/exception/JwtExpiredTokenException.java
  19. 26
      application/src/main/java/org/thingsboard/server/service/security/exception/UserPasswordNotValidException.java
  20. 12
      application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
  21. 55
      application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java
  22. 2
      application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java
  23. 3
      application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java
  24. 4
      application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
  25. 1
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetProfileExportService.java
  26. 1
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java
  27. 3
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java
  28. 1
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java
  29. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetTypeExportService.java
  30. 1
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java
  31. 33
      application/src/main/java/org/thingsboard/server/service/ws/AuthCmd.java
  32. 410
      application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
  33. 11
      application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java
  34. 22
      application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java
  35. 18
      application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionType.java
  36. 10
      application/src/main/java/org/thingsboard/server/service/ws/WsCmd.java
  37. 39
      application/src/main/java/org/thingsboard/server/service/ws/WsCmdType.java
  38. 71
      application/src/main/java/org/thingsboard/server/service/ws/WsCommandsWrapper.java
  39. 2
      application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java
  40. 7
      application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/MarkAllNotificationsAsReadCmd.java
  41. 7
      application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/MarkNotificationsAsReadCmd.java
  42. 19
      application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationCmdsWrapper.java
  43. 7
      application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsCountSubCmd.java
  44. 7
      application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsSubCmd.java
  45. 7
      application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsUnsubCmd.java
  46. 5
      application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/UnreadNotificationsCountUpdate.java
  47. 5
      application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/UnreadNotificationsUpdate.java
  48. 1
      application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java
  49. 3
      application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java
  50. 26
      application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/TelemetryCmdsWrapper.java
  51. 6
      application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/AttributesSubscriptionCmd.java
  52. 5
      application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/GetHistoryCmd.java
  53. 2
      application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/SubscriptionCmd.java
  54. 4
      application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/TelemetryPluginCmd.java
  55. 8
      application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/TimeseriesSubscriptionCmd.java
  56. 6
      application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmCountCmd.java
  57. 5
      application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmCountUnsubscribeCmd.java
  58. 6
      application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmDataCmd.java
  59. 5
      application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmDataUnsubscribeCmd.java
  60. 3
      application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/DataCmd.java
  61. 6
      application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityCountCmd.java
  62. 5
      application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityCountUnsubscribeCmd.java
  63. 5
      application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityDataCmd.java
  64. 5
      application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityDataUnsubscribeCmd.java
  65. 4
      application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/UnsubscribeCmd.java
  66. 4
      application/src/main/resources/thingsboard.yml
  67. 13
      application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
  68. 83
      application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java
  69. 65
      application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java
  70. 43
      application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java
  71. 7
      application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java
  72. 29
      application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java
  73. 3
      application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
  74. 26
      application/src/test/java/org/thingsboard/server/service/notification/NotificationApiWsClient.java
  75. 8
      application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java
  76. 4
      application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java
  77. 7
      application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java
  78. 1
      common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java
  79. 1
      common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java
  80. 1
      common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java
  81. 3
      common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java
  82. 4
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/UserPasswordPolicy.java
  83. 1
      common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java
  84. 2
      common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java
  85. 9
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java
  86. 3
      dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java
  87. 33
      dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java
  88. 15
      dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java
  89. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java
  90. 32
      dao/src/test/java/org/thingsboard/server/dao/util/DeviceConnectivityUtilTest.java
  91. 3
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
  92. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  93. 108
      ui-ngx/src/app/core/ws/notification-websocket.service.ts
  94. 105
      ui-ngx/src/app/core/ws/telemetry-websocket.service.ts
  95. 45
      ui-ngx/src/app/core/ws/websocket.service.ts
  96. 11
      ui-ngx/src/app/modules/home/components/notification/notification-bell.component.ts
  97. 8
      ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts
  98. 4
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.models.ts
  99. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.html
  100. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.scss

2
application/src/main/data/json/system/widget_types/battery_level.json

@ -17,7 +17,7 @@
"settingsDirective": "tb-battery-level-widget-settings", "settingsDirective": "tb-battery-level-widget-settings",
"hasBasicMode": true, "hasBasicMode": true,
"basicModeDirective": "tb-battery-level-basic-config", "basicModeDirective": "tb-battery-level-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"batteryLevel\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"layout\":\"vertical_solid\",\"showValue\":true,\"autoScaleValueSize\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":20,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"24px\"},\"valueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"batteryLevelColor\":{\"color\":\"rgba(92, 223, 144, 1)\",\"type\":\"range\",\"rangeList\":[{\"from\":0,\"to\":25,\"color\":\"rgba(227, 71, 71, 1)\"},{\"from\":25,\"to\":50,\"color\":\"rgba(246, 206, 67, 1)\"},{\"from\":50,\"to\":100,\"color\":\"rgba(92, 223, 144, 1)\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"batteryShapeColor\":{\"color\":\"rgba(92, 223, 144, 0.32)\",\"type\":\"range\",\"rangeList\":[{\"from\":0,\"to\":25,\"color\":\"rgba(227, 71, 71, 0.32)\"},{\"from\":25,\"to\":50,\"color\":\"rgba(246, 206, 67, 0.32)\"},{\"from\":50,\"to\":100,\"color\":\"rgba(92, 223, 144, 0.32)\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"sectionsCount\":4},\"title\":\"Battery level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:battery-high\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"batteryLevel\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"layout\":\"vertical_solid\",\"showValue\":true,\"autoScaleValueSize\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":20,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"24px\"},\"valueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"batteryLevelColor\":{\"type\":\"range\",\"color\":\"rgb(224, 224, 224)\",\"rangeList\":[{\"from\":0,\"to\":25,\"color\":\"rgba(227, 71, 71, 1)\"},{\"from\":25,\"to\":50,\"color\":\"rgba(246, 206, 67, 1)\"},{\"from\":50,\"to\":100,\"color\":\"rgba(92, 223, 144, 1)\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"batteryShapeColor\":{\"type\":\"range\",\"color\":\"rgba(224, 224, 224, 0.32)\",\"rangeList\":[{\"from\":0,\"to\":25,\"color\":\"rgba(227, 71, 71, 0.32)\"},{\"from\":25,\"to\":50,\"color\":\"rgba(246, 206, 67, 0.32)\"},{\"from\":50,\"to\":100,\"color\":\"rgba(92, 223, 144, 0.32)\"}],\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"sectionsCount\":4},\"title\":\"Battery level\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:battery-high\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}"
}, },
"externalId": null, "externalId": null,
"tags": [ "tags": [

6
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -68,6 +68,7 @@ import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeEventService;
import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor; import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor;
@ -455,6 +456,11 @@ public class ActorSystemContext {
@Getter @Getter
private WidgetTypeService widgetTypeService; private WidgetTypeService widgetTypeService;
@Lazy
@Autowired(required = false)
@Getter
private EntityService entityService;
@Value("${actors.session.max_concurrent_sessions_per_device:1}") @Value("${actors.session.max_concurrent_sessions_per_device:1}")
@Getter @Getter
private long maxConcurrentSessionsPerDevice; private long maxConcurrentSessionsPerDevice;

6
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -84,6 +84,7 @@ import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeEventService;
import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.nosql.CassandraStatementTask; import org.thingsboard.server.dao.nosql.CassandraStatementTask;
import org.thingsboard.server.dao.nosql.TbResultSetFuture; import org.thingsboard.server.dao.nosql.TbResultSetFuture;
@ -896,6 +897,11 @@ class DefaultTbContext implements TbContext {
return mainCtx.getApiUsageStateService(); return mainCtx.getApiUsageStateService();
} }
@Override
public EntityService getEntityService() {
return mainCtx.getEntityService();
}
private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) { private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) {
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("ruleNodeId", ruleNodeId.toString()); metaData.putValue("ruleNodeId", ruleNodeId.toString());

24
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java

@ -32,14 +32,12 @@ import org.springframework.security.config.annotation.method.configuration.Enabl
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.header.writers.StaticHeadersWriter; import org.springframework.security.web.header.writers.StaticHeadersWriter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter; import org.springframework.web.filter.CorsFilter;
import org.springframework.web.filter.ShallowEtagHeaderFilter; import org.springframework.web.filter.ShallowEtagHeaderFilter;
@ -63,7 +61,7 @@ import java.util.List;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true) @EnableGlobalMethodSecurity(prePostEnabled = true)
@Order(SecurityProperties.BASIC_AUTH_ORDER) @Order(SecurityProperties.BASIC_AUTH_ORDER)
@TbCoreComponent @TbCoreComponent
public class ThingsboardSecurityConfiguration { public class ThingsboardSecurityConfiguration {
@ -79,7 +77,7 @@ public class ThingsboardSecurityConfiguration {
public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token"; public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token";
protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**"}; protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**"};
public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**"; public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**";
public static final String WS_TOKEN_BASED_AUTH_ENTRY_POINT = "/api/ws/**"; public static final String WS_ENTRY_POINT = "/api/ws/**";
public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code"; public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code";
public static final String DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT = "/api/device-connectivity/mqtts/certificate/download"; public static final String DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT = "/api/device-connectivity/mqtts/certificate/download";
@ -115,10 +113,6 @@ public class ThingsboardSecurityConfiguration {
@Qualifier("jwtHeaderTokenExtractor") @Qualifier("jwtHeaderTokenExtractor")
private TokenExtractor jwtHeaderTokenExtractor; private TokenExtractor jwtHeaderTokenExtractor;
@Autowired
@Qualifier("jwtQueryTokenExtractor")
private TokenExtractor jwtQueryTokenExtractor;
@Autowired private AuthenticationManager authenticationManager; @Autowired private AuthenticationManager authenticationManager;
@Autowired private RateLimitProcessingFilter rateLimitProcessingFilter; @Autowired private RateLimitProcessingFilter rateLimitProcessingFilter;
@ -150,7 +144,7 @@ public class ThingsboardSecurityConfiguration {
protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception { protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception {
List<String> pathsToSkip = new ArrayList<>(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS)); List<String> pathsToSkip = new ArrayList<>(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS));
pathsToSkip.addAll(Arrays.asList(WS_TOKEN_BASED_AUTH_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT, pathsToSkip.addAll(Arrays.asList(WS_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT,
PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT, MAIL_OAUTH2_PROCESSING_ENTRY_POINT, PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT, MAIL_OAUTH2_PROCESSING_ENTRY_POINT,
DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT)); DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT));
SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT); SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT);
@ -167,15 +161,6 @@ public class ThingsboardSecurityConfiguration {
return filter; return filter;
} }
@Bean
protected JwtTokenAuthenticationProcessingFilter buildWsJwtTokenAuthenticationProcessingFilter() throws Exception {
AntPathRequestMatcher matcher = new AntPathRequestMatcher(WS_TOKEN_BASED_AUTH_ENTRY_POINT);
JwtTokenAuthenticationProcessingFilter filter
= new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtQueryTokenExtractor, matcher);
filter.setAuthenticationManager(this.authenticationManager);
return filter;
}
@Bean @Bean
public AuthenticationManager authenticationManager(ObjectPostProcessor<Object> objectPostProcessor) throws Exception { public AuthenticationManager authenticationManager(ObjectPostProcessor<Object> objectPostProcessor) throws Exception {
DefaultAuthenticationEventPublisher eventPublisher = objectPostProcessor DefaultAuthenticationEventPublisher eventPublisher = objectPostProcessor
@ -229,7 +214,7 @@ public class ThingsboardSecurityConfiguration {
.antMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points .antMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points
.and() .and()
.authorizeRequests() .authorizeRequests()
.antMatchers(WS_TOKEN_BASED_AUTH_ENTRY_POINT).authenticated() // Protected WebSocket API End-points .antMatchers(WS_ENTRY_POINT).permitAll() // WebSocket API End-points
.antMatchers(TOKEN_BASED_AUTH_ENTRY_POINT).authenticated() // Protected API End-points .antMatchers(TOKEN_BASED_AUTH_ENTRY_POINT).authenticated() // Protected API End-points
.and() .and()
.exceptionHandling().accessDeniedHandler(restAccessDeniedHandler) .exceptionHandling().accessDeniedHandler(restAccessDeniedHandler)
@ -238,7 +223,6 @@ public class ThingsboardSecurityConfiguration {
.addFilterBefore(buildRestPublicLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildRestPublicLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class); .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class);
if (oauth2Configuration != null) { if (oauth2Configuration != null) {
http.oauth2Login() http.oauth2Login()

51
application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java

@ -19,25 +19,13 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean; import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.controller.plugin.TbWebSocketHandler; import org.thingsboard.server.controller.plugin.TbWebSocketHandler;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.Map;
@Configuration @Configuration
@TbCoreComponent @TbCoreComponent
@ -46,8 +34,9 @@ import java.util.Map;
@Slf4j @Slf4j
public class WebSocketConfiguration implements WebSocketConfigurer { public class WebSocketConfiguration implements WebSocketConfigurer {
public static final String WS_PLUGIN_PREFIX = "/api/ws/plugins/"; public static final String WS_API_ENDPOINT = "/api/ws";
private static final String WS_PLUGIN_MAPPING = WS_PLUGIN_PREFIX + "**"; public static final String WS_PLUGINS_ENDPOINT = "/api/ws/plugins/";
private static final String WS_API_MAPPING = "/api/ws/**";
private final WebSocketHandler wsHandler; private final WebSocketHandler wsHandler;
@ -65,39 +54,7 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
log.error("TbWebSocketHandler expected but [{}] provided", wsHandler); log.error("TbWebSocketHandler expected but [{}] provided", wsHandler);
throw new RuntimeException("TbWebSocketHandler expected but " + wsHandler + " provided"); throw new RuntimeException("TbWebSocketHandler expected but " + wsHandler + " provided");
} }
registry.addHandler(wsHandler, WS_PLUGIN_MAPPING).setAllowedOriginPatterns("*") registry.addHandler(wsHandler, WS_API_MAPPING).setAllowedOriginPatterns("*");
.addInterceptors(new HttpSessionHandshakeInterceptor(), new HandshakeInterceptor() {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
SecurityUser user = null;
try {
user = getCurrentUser();
} catch (ThingsboardException ex) {
}
if (user == null) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return false;
} else {
return true;
}
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception exception) {
//Do nothing
}
});
} }
protected SecurityUser getCurrentUser() throws ThingsboardException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof SecurityUser) {
return (SecurityUser) authentication.getPrincipal();
} else {
throw new ThingsboardException("You aren't authorized to perform this operation!", ThingsboardErrorCode.AUTHENTICATION);
}
}
} }

8
application/src/main/java/org/thingsboard/server/controller/ImageController.java

@ -50,9 +50,10 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.resource.ImageCacheKey;
import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.service.validator.ResourceDataValidator;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.dao.resource.ImageCacheKey;
import org.thingsboard.server.service.resource.TbImageService; import org.thingsboard.server.service.resource.TbImageService;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
@ -77,6 +78,8 @@ public class ImageController extends BaseController {
private final ImageService imageService; private final ImageService imageService;
private final TbImageService tbImageService; private final TbImageService tbImageService;
private final ResourceDataValidator resourceValidator;
@Value("${cache.image.systemImagesBrowserTtlInMinutes:0}") @Value("${cache.image.systemImagesBrowserTtlInMinutes:0}")
private int systemImagesBrowserTtlInMinutes; private int systemImagesBrowserTtlInMinutes;
@Value("${cache.image.tenantImagesBrowserTtlInMinutes:0}") @Value("${cache.image.tenantImagesBrowserTtlInMinutes:0}")
@ -94,6 +97,7 @@ public class ImageController extends BaseController {
TbResource image = new TbResource(); TbResource image = new TbResource();
image.setTenantId(user.getTenantId()); image.setTenantId(user.getTenantId());
accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.CREATE, null, image); accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.CREATE, null, image);
resourceValidator.validateResourceSize(user.getTenantId(), null, file.getSize());
image.setFileName(file.getOriginalFilename()); image.setFileName(file.getOriginalFilename());
if (StringUtils.isNotEmpty(title)) { if (StringUtils.isNotEmpty(title)) {
@ -115,6 +119,8 @@ public class ImageController extends BaseController {
@PathVariable String key, @PathVariable String key,
@RequestPart MultipartFile file) throws Exception { @RequestPart MultipartFile file) throws Exception {
TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.WRITE); TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.WRITE);
resourceValidator.validateResourceSize(getTenantId(), imageInfo.getId(), file.getSize());
TbResource image = new TbResource(imageInfo); TbResource image = new TbResource(imageInfo);
image.setData(file.getBytes()); image.setData(file.getBytes());
image.setFileName(file.getOriginalFilename()); image.setFileName(file.getOriginalFilename());

320
application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java

@ -15,12 +15,17 @@
*/ */
package org.thingsboard.server.controller.plugin; package org.thingsboard.server.controller.plugin;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.BeanCreationNotAllowedException; import org.springframework.beans.factory.BeanCreationNotAllowedException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.PongMessage; import org.springframework.web.socket.PongMessage;
@ -28,6 +33,7 @@ import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.adapter.NativeWebSocketSession; import org.springframework.web.socket.adapter.NativeWebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler; import org.springframework.web.socket.handler.TextWebSocketHandler;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
@ -39,49 +45,59 @@ import org.thingsboard.server.config.WebSocketConfiguration;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.dao.util.limits.RateLimitService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.subscription.SubscriptionErrorCode;
import org.thingsboard.server.service.ws.AuthCmd;
import org.thingsboard.server.service.ws.SessionEvent; import org.thingsboard.server.service.ws.SessionEvent;
import org.thingsboard.server.service.ws.WebSocketMsgEndpoint; import org.thingsboard.server.service.ws.WebSocketMsgEndpoint;
import org.thingsboard.server.service.ws.WebSocketService; import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.WebSocketSessionRef; import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.WebSocketSessionType; import org.thingsboard.server.service.ws.WebSocketSessionType;
import org.thingsboard.server.service.ws.WsCommandsWrapper;
import org.thingsboard.server.service.ws.notification.cmd.NotificationCmdsWrapper;
import org.thingsboard.server.service.ws.telemetry.cmd.TelemetryCmdsWrapper;
import javax.annotation.PostConstruct;
import javax.websocket.RemoteEndpoint; import javax.websocket.RemoteEndpoint;
import javax.websocket.SendHandler; import javax.websocket.SendHandler;
import javax.websocket.SendResult; import javax.websocket.SendResult;
import javax.websocket.Session; import javax.websocket.Session;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.security.InvalidParameterException; import java.security.InvalidParameterException;
import java.util.Optional; import java.util.Optional;
import java.util.Queue; import java.util.Queue;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static org.thingsboard.server.service.ws.DefaultWebSocketService.NUMBER_OF_PING_ATTEMPTS; import static org.thingsboard.server.service.ws.DefaultWebSocketService.NUMBER_OF_PING_ATTEMPTS;
@Service @Service
@TbCoreComponent @TbCoreComponent
@Slf4j @Slf4j
@RequiredArgsConstructor
public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocketMsgEndpoint { public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocketMsgEndpoint {
private final ConcurrentMap<String, SessionMetaData> internalSessionMap = new ConcurrentHashMap<>(); private final ConcurrentMap<String, SessionMetaData> internalSessionMap = new ConcurrentHashMap<>();
private final ConcurrentMap<String, String> externalSessionMap = new ConcurrentHashMap<>(); private final ConcurrentMap<String, String> externalSessionMap = new ConcurrentHashMap<>();
@Autowired @Lazy @Autowired @Lazy
private WebSocketService webSocketService; private WebSocketService webSocketService;
@Autowired @Autowired
private TbTenantProfileCache tenantProfileCache; private TbTenantProfileCache tenantProfileCache;
@Autowired @Autowired
private RateLimitService rateLimitService; private RateLimitService rateLimitService;
@Autowired
private JwtAuthenticationProvider authenticationProvider;
@Value("${server.ws.send_timeout:5000}") @Value("${server.ws.send_timeout:5000}")
private long sendTimeout; private long sendTimeout;
@ -89,6 +105,8 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
private long pingTimeout; private long pingTimeout;
@Value("${server.ws.max_queue_messages_per_session:1000}") @Value("${server.ws.max_queue_messages_per_session:1000}")
private int wsMaxQueueMessagesPerSession; private int wsMaxQueueMessagesPerSession;
@Value("${server.ws.auth_timeout_ms:10000}")
private int authTimeoutMs;
private final ConcurrentMap<String, WebSocketSessionRef> blacklistedSessions = new ConcurrentHashMap<>(); private final ConcurrentMap<String, WebSocketSessionRef> blacklistedSessions = new ConcurrentHashMap<>();
@ -97,28 +115,97 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
private final ConcurrentMap<UserId, Set<String>> regularUserSessionsMap = new ConcurrentHashMap<>(); private final ConcurrentMap<UserId, Set<String>> regularUserSessionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<UserId, Set<String>> publicUserSessionsMap = new ConcurrentHashMap<>(); private final ConcurrentMap<UserId, Set<String>> publicUserSessionsMap = new ConcurrentHashMap<>();
private Cache<String, SessionMetaData> pendingSessions;
@PostConstruct
private void init() {
pendingSessions = Caffeine.newBuilder()
.expireAfterWrite(authTimeoutMs, TimeUnit.MILLISECONDS)
.<String, SessionMetaData>removalListener((sessionId, sessionMd, removalCause) -> {
if (removalCause == RemovalCause.EXPIRED && sessionMd != null) {
try {
close(sessionMd.sessionRef, CloseStatus.POLICY_VIOLATION);
} catch (IOException e) {
log.warn("IO error", e);
}
}
})
.build();
}
@Override @Override
public void handleTextMessage(WebSocketSession session, TextMessage message) { public void handleTextMessage(WebSocketSession session, TextMessage message) {
try { try {
SessionMetaData sessionMd = internalSessionMap.get(session.getId()); SessionMetaData sessionMd = getSessionMd(session.getId());
if (sessionMd != null) { if (sessionMd == null) {
log.trace("[{}][{}] Processing {}", sessionMd.sessionRef.getSecurityCtx().getTenantId(), session.getId(), message.getPayload());
webSocketService.handleWebSocketMsg(sessionMd.sessionRef, message.getPayload());
} else {
log.trace("[{}] Failed to find session", session.getId()); log.trace("[{}] Failed to find session", session.getId());
session.close(CloseStatus.SERVER_ERROR.withReason("Session not found!")); session.close(CloseStatus.SERVER_ERROR.withReason("Session not found!"));
return;
} }
sessionMd.onMsg(message.getPayload());
} catch (IOException e) { } catch (IOException e) {
log.warn("IO error", e); log.warn("IO error", e);
} }
} }
void processMsg(SessionMetaData sessionMd, String msg) throws IOException {
WebSocketSessionRef sessionRef = sessionMd.sessionRef;
WsCommandsWrapper cmdsWrapper;
try {
switch (sessionRef.getSessionType()) {
case GENERAL:
cmdsWrapper = JacksonUtil.fromString(msg, WsCommandsWrapper.class);
break;
case TELEMETRY:
cmdsWrapper = JacksonUtil.fromString(msg, TelemetryCmdsWrapper.class).toCommonCmdsWrapper();
break;
case NOTIFICATIONS:
cmdsWrapper = JacksonUtil.fromString(msg, NotificationCmdsWrapper.class).toCommonCmdsWrapper();
break;
default:
return;
}
} catch (Exception e) {
log.debug("{} Failed to decode subscription cmd: {}", sessionRef, e.getMessage(), e);
if (sessionRef.getSecurityCtx() != null) {
webSocketService.sendError(sessionRef, 1, SubscriptionErrorCode.BAD_REQUEST, "Failed to parse the payload");
} else {
close(sessionRef, CloseStatus.BAD_DATA.withReason(e.getMessage()));
}
return;
}
if (sessionRef.getSecurityCtx() != null) {
log.trace("{} Processing {}", sessionRef, msg);
webSocketService.handleCommands(sessionRef, cmdsWrapper);
} else {
AuthCmd authCmd = cmdsWrapper.getAuthCmd();
if (authCmd == null) {
close(sessionRef, CloseStatus.POLICY_VIOLATION.withReason("Auth cmd is missing"));
return;
}
log.trace("{} Authenticating session", sessionRef);
SecurityUser securityCtx;
try {
securityCtx = authenticationProvider.authenticate(authCmd.getToken());
} catch (Exception e) {
close(sessionRef, CloseStatus.BAD_DATA.withReason(e.getMessage()));
return;
}
sessionRef.setSecurityCtx(securityCtx);
pendingSessions.invalidate(sessionMd.session.getId());
establishSession(sessionMd.session, sessionRef, sessionMd);
webSocketService.handleCommands(sessionRef, cmdsWrapper);
}
}
@Override @Override
protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception { protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {
try { try {
SessionMetaData sessionMd = internalSessionMap.get(session.getId()); SessionMetaData sessionMd = getSessionMd(session.getId());
if (sessionMd != null) { if (sessionMd != null) {
log.trace("[{}][{}] Processing pong response {}", sessionMd.sessionRef.getSecurityCtx().getTenantId(), session.getId(), message.getPayload()); log.trace("{} Processing pong response {}", sessionMd.sessionRef, message.getPayload());
sessionMd.processPongMessage(System.currentTimeMillis()); sessionMd.processPongMessage(System.currentTimeMillis());
} else { } else {
log.trace("[{}] Failed to find session", session.getId()); log.trace("[{}] Failed to find session", session.getId());
@ -139,23 +226,9 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
nativeSession.getAsyncRemote().setSendTimeout(sendTimeout); nativeSession.getAsyncRemote().setSendTimeout(sendTimeout);
} }
} }
String internalSessionId = session.getId();
WebSocketSessionRef sessionRef = toRef(session); WebSocketSessionRef sessionRef = toRef(session);
String externalSessionId = sessionRef.getSessionId(); log.debug("[{}][{}] Session opened from address: {}", sessionRef.getSessionId(), session.getId(), session.getRemoteAddress());
establishSession(session, sessionRef, null);
if (!checkLimits(session, sessionRef)) {
return;
}
var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef);
int wsTenantProfileQueueLimit = tenantProfileConfiguration != null ?
tenantProfileConfiguration.getWsMsgQueueLimitPerSession() : wsMaxQueueMessagesPerSession;
internalSessionMap.put(internalSessionId, new SessionMetaData(session, sessionRef,
(wsTenantProfileQueueLimit > 0 && wsTenantProfileQueueLimit < wsMaxQueueMessagesPerSession) ?
wsTenantProfileQueueLimit : wsMaxQueueMessagesPerSession));
externalSessionMap.put(externalSessionId, internalSessionId);
processInWebSocketService(sessionRef, SessionEvent.onEstablished());
log.info("[{}][{}][{}] Session is opened from address: {}", sessionRef.getSecurityCtx().getTenantId(), externalSessionId, session.getId(), session.getRemoteAddress());
} catch (InvalidParameterException e) { } catch (InvalidParameterException e) {
log.warn("[{}] Failed to start session", session.getId(), e); log.warn("[{}] Failed to start session", session.getId(), e);
session.close(CloseStatus.BAD_DATA.withReason(e.getMessage())); session.close(CloseStatus.BAD_DATA.withReason(e.getMessage()));
@ -165,10 +238,36 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
} }
} }
private void establishSession(WebSocketSession session, WebSocketSessionRef sessionRef, SessionMetaData sessionMd) throws IOException {
if (sessionRef.getSecurityCtx() != null) {
if (!checkLimits(session, sessionRef)) {
return;
}
int maxMsgQueueSize = Optional.ofNullable(getTenantProfileConfiguration(sessionRef))
.map(DefaultTenantProfileConfiguration::getWsMsgQueueLimitPerSession)
.filter(profileLimit -> profileLimit > 0 && profileLimit < wsMaxQueueMessagesPerSession)
.orElse(wsMaxQueueMessagesPerSession);
if (sessionMd == null) {
sessionMd = new SessionMetaData(session, sessionRef);
}
sessionMd.setMaxMsgQueueSize(maxMsgQueueSize);
internalSessionMap.put(session.getId(), sessionMd);
externalSessionMap.put(sessionRef.getSessionId(), session.getId());
processInWebSocketService(sessionRef, SessionEvent.onEstablished());
log.info("[{}][{}][{}][{}] Session established from address: {}", sessionRef.getSecurityCtx().getTenantId(),
sessionRef.getSecurityCtx().getId(), sessionRef.getSessionId(), session.getId(), session.getRemoteAddress());
} else {
sessionMd = new SessionMetaData(session, sessionRef);
pendingSessions.put(session.getId(), sessionMd);
externalSessionMap.put(sessionRef.getSessionId(), session.getId());
}
}
@Override @Override
public void handleTransportError(WebSocketSession session, Throwable tError) throws Exception { public void handleTransportError(WebSocketSession session, Throwable tError) throws Exception {
super.handleTransportError(session, tError); super.handleTransportError(session, tError);
SessionMetaData sessionMd = internalSessionMap.get(session.getId()); SessionMetaData sessionMd = getSessionMd(session.getId());
if (sessionMd != null) { if (sessionMd != null) {
processInWebSocketService(sessionMd.sessionRef, SessionEvent.onError(tError)); processInWebSocketService(sessionMd.sessionRef, SessionEvent.onError(tError));
} else { } else {
@ -181,63 +280,87 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception { public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
super.afterConnectionClosed(session, closeStatus); super.afterConnectionClosed(session, closeStatus);
SessionMetaData sessionMd = internalSessionMap.remove(session.getId()); SessionMetaData sessionMd = internalSessionMap.remove(session.getId());
if (sessionMd == null) {
sessionMd = pendingSessions.asMap().remove(session.getId());
}
if (sessionMd != null) { if (sessionMd != null) {
cleanupLimits(session, sessionMd.sessionRef);
externalSessionMap.remove(sessionMd.sessionRef.getSessionId()); externalSessionMap.remove(sessionMd.sessionRef.getSessionId());
processInWebSocketService(sessionMd.sessionRef, SessionEvent.onClosed()); if (sessionMd.sessionRef.getSecurityCtx() != null) {
log.info("[{}][{}][{}] Session is closed", sessionMd.sessionRef.getSecurityCtx().getTenantId(), sessionMd.sessionRef.getSessionId(), session.getId()); cleanupLimits(session, sessionMd.sessionRef);
processInWebSocketService(sessionMd.sessionRef, SessionEvent.onClosed());
}
log.info("{} Session is closed", sessionMd.sessionRef);
} else { } else {
log.info("[{}] Session is closed", session.getId()); log.info("[{}] Session is closed", session.getId());
} }
} }
private void processInWebSocketService(WebSocketSessionRef sessionRef, SessionEvent event) { private void processInWebSocketService(WebSocketSessionRef sessionRef, SessionEvent event) {
if (sessionRef.getSecurityCtx() == null) {
return;
}
try { try {
webSocketService.handleWebSocketSessionEvent(sessionRef, event); webSocketService.handleSessionEvent(sessionRef, event);
} catch (BeanCreationNotAllowedException e) { } catch (BeanCreationNotAllowedException e) {
log.warn("[{}] Failed to close session due to possible shutdown state", sessionRef.getSessionId()); log.warn("{} Failed to close session due to possible shutdown state", sessionRef);
} }
} }
private WebSocketSessionRef toRef(WebSocketSession session) throws IOException { private WebSocketSessionRef toRef(WebSocketSession session) {
URI sessionUri = session.getUri(); String path = session.getUri().getPath();
String path = sessionUri.getPath(); WebSocketSessionType sessionType;
path = path.substring(WebSocketConfiguration.WS_PLUGIN_PREFIX.length()); if (path.equals(WebSocketConfiguration.WS_API_ENDPOINT)) {
if (path.length() == 0) { sessionType = WebSocketSessionType.GENERAL;
throw new IllegalArgumentException("URL should contain plugin token!"); } else {
String type = StringUtils.substringAfter(path, WebSocketConfiguration.WS_PLUGINS_ENDPOINT);
sessionType = WebSocketSessionType.forName(type)
.orElseThrow(() -> new InvalidParameterException("Unknown session type"));
} }
String[] pathElements = path.split("/");
String serviceToken = pathElements[0];
WebSocketSessionType sessionType = WebSocketSessionType.forName(serviceToken)
.orElseThrow(() -> new InvalidParameterException("Can't find plugin with specified token!"));
SecurityUser currentUser = (SecurityUser) ((Authentication) session.getPrincipal()).getPrincipal(); SecurityUser securityCtx = null;
String token = StringUtils.substringAfter(session.getUri().getQuery(), "token=");
if (StringUtils.isNotEmpty(token)) {
securityCtx = authenticationProvider.authenticate(token);
}
return WebSocketSessionRef.builder() return WebSocketSessionRef.builder()
.sessionId(UUID.randomUUID().toString()) .sessionId(UUID.randomUUID().toString())
.securityCtx(currentUser) .securityCtx(securityCtx)
.localAddress(session.getLocalAddress()) .localAddress(session.getLocalAddress())
.remoteAddress(session.getRemoteAddress()) .remoteAddress(session.getRemoteAddress())
.sessionType(sessionType) .sessionType(sessionType)
.build(); .build();
} }
private SessionMetaData getSessionMd(String internalSessionId) {
SessionMetaData sessionMd = internalSessionMap.get(internalSessionId);
if (sessionMd == null) {
sessionMd = pendingSessions.getIfPresent(internalSessionId);
}
return sessionMd;
}
class SessionMetaData implements SendHandler { class SessionMetaData implements SendHandler {
private final WebSocketSession session; private final WebSocketSession session;
private final RemoteEndpoint.Async asyncRemote; private final RemoteEndpoint.Async asyncRemote;
private final WebSocketSessionRef sessionRef; private final WebSocketSessionRef sessionRef;
final AtomicBoolean isSending = new AtomicBoolean(false); final AtomicBoolean isSending = new AtomicBoolean(false);
private final Queue<TbWebSocketMsg<?>> msgQueue; private final Queue<TbWebSocketMsg<?>> outboundMsgQueue = new ConcurrentLinkedQueue<>();
private final AtomicInteger outboundMsgQueueSize = new AtomicInteger();
@Setter
private int maxMsgQueueSize = wsMaxQueueMessagesPerSession;
private final Queue<String> inboundMsgQueue = new ConcurrentLinkedQueue<>();
private final Lock inboundMsgQueueProcessorLock = new ReentrantLock();
private volatile long lastActivityTime; private volatile long lastActivityTime;
SessionMetaData(WebSocketSession session, WebSocketSessionRef sessionRef, int maxMsgQueuePerSession) { SessionMetaData(WebSocketSession session, WebSocketSessionRef sessionRef) {
super(); super();
this.session = session; this.session = session;
Session nativeSession = ((NativeWebSocketSession) session).getNativeSession(Session.class); Session nativeSession = ((NativeWebSocketSession) session).getNativeSession(Session.class);
this.asyncRemote = nativeSession.getAsyncRemote(); this.asyncRemote = nativeSession.getAsyncRemote();
this.sessionRef = sessionRef; this.sessionRef = sessionRef;
this.msgQueue = new LinkedBlockingQueue<>(maxMsgQueuePerSession);
this.lastActivityTime = System.currentTimeMillis(); this.lastActivityTime = System.currentTimeMillis();
} }
@ -245,13 +368,13 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
try { try {
long timeSinceLastActivity = currentTime - lastActivityTime; long timeSinceLastActivity = currentTime - lastActivityTime;
if (timeSinceLastActivity >= pingTimeout) { if (timeSinceLastActivity >= pingTimeout) {
log.warn("[{}] Closing session due to ping timeout", session.getId()); log.warn("{} Closing session due to ping timeout", sessionRef);
closeSession(CloseStatus.SESSION_NOT_RELIABLE); closeSession(CloseStatus.SESSION_NOT_RELIABLE);
} else if (timeSinceLastActivity >= pingTimeout / NUMBER_OF_PING_ATTEMPTS) { } else if (timeSinceLastActivity >= pingTimeout / NUMBER_OF_PING_ATTEMPTS) {
sendMsg(TbWebSocketPingMsg.INSTANCE); sendMsg(TbWebSocketPingMsg.INSTANCE);
} }
} catch (Exception e) { } catch (Exception e) {
log.trace("[{}] Failed to send ping msg", session.getId(), e); log.trace("{} Failed to send ping msg", sessionRef, e);
closeSession(CloseStatus.SESSION_NOT_RELIABLE); closeSession(CloseStatus.SESSION_NOT_RELIABLE);
} }
} }
@ -260,9 +383,9 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
try { try {
close(this.sessionRef, reason); close(this.sessionRef, reason);
} catch (IOException ioe) { } catch (IOException ioe) {
log.trace("[{}] Session transport error", session.getId(), ioe); log.trace("{} Session transport error", sessionRef, ioe);
} finally { } finally {
msgQueue.clear(); outboundMsgQueue.clear();
} }
} }
@ -275,19 +398,14 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
} }
void sendMsg(TbWebSocketMsg<?> msg) { void sendMsg(TbWebSocketMsg<?> msg) {
try { if (outboundMsgQueueSize.get() < maxMsgQueueSize) {
msgQueue.add(msg); outboundMsgQueue.add(msg);
} catch (RuntimeException e) { outboundMsgQueueSize.incrementAndGet();
if (log.isTraceEnabled()) { processNextMsg();
log.trace("[{}][{}] Session closed due to queue error", sessionRef.getSecurityCtx().getTenantId(), session.getId(), e); } else {
} else { log.info("{} Session closed due to updates queue size exceeded", sessionRef);
log.info("[{}][{}] Session closed due to queue error", sessionRef.getSecurityCtx().getTenantId(), session.getId());
}
closeSession(CloseStatus.POLICY_VIOLATION.withReason("Max pending updates limit reached!")); closeSession(CloseStatus.POLICY_VIOLATION.withReason("Max pending updates limit reached!"));
return;
} }
processNextMsg();
} }
private void sendMsgInternal(TbWebSocketMsg<?> msg) { private void sendMsgInternal(TbWebSocketMsg<?> msg) {
@ -303,7 +421,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
processNextMsg(); processNextMsg();
} }
} catch (Exception e) { } catch (Exception e) {
log.trace("[{}] Failed to send msg", session.getId(), e); log.trace("{} Failed to send msg", sessionRef, e);
closeSession(CloseStatus.SESSION_NOT_RELIABLE); closeSession(CloseStatus.SESSION_NOT_RELIABLE);
} }
} }
@ -311,7 +429,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
@Override @Override
public void onResult(SendResult result) { public void onResult(SendResult result) {
if (!result.isOK()) { if (!result.isOK()) {
log.trace("[{}] Failed to send msg", session.getId(), result.getException()); log.trace("{} Failed to send msg", sessionRef, result.getException());
closeSession(CloseStatus.SESSION_NOT_RELIABLE); closeSession(CloseStatus.SESSION_NOT_RELIABLE);
return; return;
} }
@ -321,22 +439,45 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
} }
private void processNextMsg() { private void processNextMsg() {
if (msgQueue.isEmpty() || !isSending.compareAndSet(false, true)) { if (outboundMsgQueue.isEmpty() || !isSending.compareAndSet(false, true)) {
return; return;
} }
TbWebSocketMsg<?> msg = msgQueue.poll(); TbWebSocketMsg<?> msg = outboundMsgQueue.poll();
if (msg != null) { if (msg != null) {
outboundMsgQueueSize.decrementAndGet();
sendMsgInternal(msg); sendMsgInternal(msg);
} else { } else {
isSending.set(false); isSending.set(false);
} }
} }
public void onMsg(String msg) throws IOException {
inboundMsgQueue.add(msg);
tryProcessInboundMsgs();
}
void tryProcessInboundMsgs() throws IOException {
while (!inboundMsgQueue.isEmpty()) {
if (inboundMsgQueueProcessorLock.tryLock()) {
try {
String msg;
while ((msg = inboundMsgQueue.poll()) != null) {
processMsg(this, msg);
}
} finally {
inboundMsgQueueProcessorLock.unlock();
}
} else {
return;
}
}
}
} }
@Override @Override
public void send(WebSocketSessionRef sessionRef, int subscriptionId, String msg) throws IOException { public void send(WebSocketSessionRef sessionRef, int subscriptionId, String msg) throws IOException {
log.debug("{} Sending {}", sessionRef, msg);
String externalId = sessionRef.getSessionId(); String externalId = sessionRef.getSessionId();
log.debug("[{}] Processing {}", externalId, msg);
String internalId = externalSessionMap.get(externalId); String internalId = externalSessionMap.get(externalId);
if (internalId != null) { if (internalId != null) {
SessionMetaData sessionMd = internalSessionMap.get(internalId); SessionMetaData sessionMd = internalSessionMap.get(internalId);
@ -344,13 +485,12 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
TenantId tenantId = sessionRef.getSecurityCtx().getTenantId(); TenantId tenantId = sessionRef.getSecurityCtx().getTenantId();
if (!rateLimitService.checkRateLimit(LimitedApi.WS_UPDATES_PER_SESSION, tenantId, (Object) sessionRef.getSessionId())) { if (!rateLimitService.checkRateLimit(LimitedApi.WS_UPDATES_PER_SESSION, tenantId, (Object) sessionRef.getSessionId())) {
if (blacklistedSessions.putIfAbsent(externalId, sessionRef) == null) { if (blacklistedSessions.putIfAbsent(externalId, sessionRef) == null) {
log.info("[{}][{}][{}] Failed to process session update. Max session updates limit reached" log.info("{} Failed to process session update. Max session updates limit reached", sessionRef);
, tenantId, sessionRef.getSecurityCtx().getId(), externalId);
sessionMd.sendMsg("{\"subscriptionId\":" + subscriptionId + ", \"errorCode\":" + ThingsboardErrorCode.TOO_MANY_UPDATES.getErrorCode() + ", \"errorMsg\":\"Too many updates!\"}"); sessionMd.sendMsg("{\"subscriptionId\":" + subscriptionId + ", \"errorCode\":" + ThingsboardErrorCode.TOO_MANY_UPDATES.getErrorCode() + ", \"errorMsg\":\"Too many updates!\"}");
} }
return; return;
} else { } else {
log.debug("[{}][{}][{}] Session is no longer blacklisted.", tenantId, sessionRef.getSecurityCtx().getId(), externalId); log.debug("{} Session is no longer blacklisted.", sessionRef);
blacklistedSessions.remove(externalId); blacklistedSessions.remove(externalId);
} }
sessionMd.sendMsg(msg); sessionMd.sendMsg(msg);
@ -381,10 +521,10 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
@Override @Override
public void close(WebSocketSessionRef sessionRef, CloseStatus reason) throws IOException { public void close(WebSocketSessionRef sessionRef, CloseStatus reason) throws IOException {
String externalId = sessionRef.getSessionId(); String externalId = sessionRef.getSessionId();
log.debug("[{}] Processing close request", externalId); log.debug("{} Processing close request", sessionRef.toString());
String internalId = externalSessionMap.get(externalId); String internalId = externalSessionMap.get(externalId);
if (internalId != null) { if (internalId != null) {
SessionMetaData sessionMd = internalSessionMap.get(internalId); SessionMetaData sessionMd = getSessionMd(internalId);
if (sessionMd != null) { if (sessionMd != null) {
sessionMd.session.close(reason); sessionMd.session.close(reason);
} else { } else {
@ -395,7 +535,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
} }
} }
private boolean checkLimits(WebSocketSession session, WebSocketSessionRef sessionRef) throws Exception { private boolean checkLimits(WebSocketSession session, WebSocketSessionRef sessionRef) throws IOException {
var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef); var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef);
if (tenantProfileConfiguration == null) { if (tenantProfileConfiguration == null) {
return true; return true;
@ -411,10 +551,9 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
} }
} }
if (!limitAllowed) { if (!limitAllowed) {
log.info("[{}][{}][{}] Failed to start session. Max tenant sessions limit reached" log.info("{} Failed to start session. Max tenant sessions limit reached", sessionRef.toString());
, sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId); session.close(CloseStatus.POLICY_VIOLATION.withReason("Max tenant sessions limit reached!"));
session.close(CloseStatus.POLICY_VIOLATION.withReason("Max tenant sessions limit reached!")); return false;
return false;
} }
} }
@ -428,10 +567,9 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
} }
} }
if (!limitAllowed) { if (!limitAllowed) {
log.info("[{}][{}][{}] Failed to start session. Max customer sessions limit reached" log.info("{} Failed to start session. Max customer sessions limit reached", sessionRef.toString());
, sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId); session.close(CloseStatus.POLICY_VIOLATION.withReason("Max customer sessions limit reached"));
session.close(CloseStatus.POLICY_VIOLATION.withReason("Max customer sessions limit reached")); return false;
return false;
} }
} }
if (tenantProfileConfiguration.getMaxWsSessionsPerRegularUser() > 0 if (tenantProfileConfiguration.getMaxWsSessionsPerRegularUser() > 0
@ -444,10 +582,9 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
} }
} }
if (!limitAllowed) { if (!limitAllowed) {
log.info("[{}][{}][{}] Failed to start session. Max regular user sessions limit reached" log.info("{} Failed to start session. Max regular user sessions limit reached", sessionRef.toString());
, sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId); session.close(CloseStatus.POLICY_VIOLATION.withReason("Max regular user sessions limit reached"));
session.close(CloseStatus.POLICY_VIOLATION.withReason("Max regular user sessions limit reached")); return false;
return false;
} }
} }
if (tenantProfileConfiguration.getMaxWsSessionsPerPublicUser() > 0 if (tenantProfileConfiguration.getMaxWsSessionsPerPublicUser() > 0
@ -460,10 +597,9 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
} }
} }
if (!limitAllowed) { if (!limitAllowed) {
log.info("[{}][{}][{}] Failed to start session. Max public user sessions limit reached" log.info("{} Failed to start session. Max public user sessions limit reached", sessionRef.toString());
, sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId); session.close(CloseStatus.POLICY_VIOLATION.withReason("Max public user sessions limit reached"));
session.close(CloseStatus.POLICY_VIOLATION.withReason("Max public user sessions limit reached")); return false;
return false;
} }
} }
} }

33
application/src/main/java/org/thingsboard/server/exception/ThingsboardCredentialsViolationResponse.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2023 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.exception;
import io.swagger.annotations.ApiModel;
import org.springframework.http.HttpStatus;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
@ApiModel
public class ThingsboardCredentialsViolationResponse extends ThingsboardErrorResponse {
protected ThingsboardCredentialsViolationResponse(String message) {
super(message, ThingsboardErrorCode.PASSWORD_VIOLATION, HttpStatus.UNAUTHORIZED);
}
public static ThingsboardCredentialsViolationResponse of(final String message) {
return new ThingsboardCredentialsViolationResponse(message);
}
}

4
application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java

@ -41,6 +41,7 @@ import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; import org.thingsboard.server.service.security.exception.UserPasswordExpiredException;
import org.thingsboard.server.service.security.exception.UserPasswordNotValidException;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -191,6 +192,9 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
UserPasswordExpiredException expiredException = (UserPasswordExpiredException) authenticationException; UserPasswordExpiredException expiredException = (UserPasswordExpiredException) authenticationException;
String resetToken = expiredException.getResetToken(); String resetToken = expiredException.getResetToken();
JacksonUtil.writeValue(response.getWriter(), ThingsboardCredentialsExpiredResponse.of(expiredException.getMessage(), resetToken)); JacksonUtil.writeValue(response.getWriter(), ThingsboardCredentialsExpiredResponse.of(expiredException.getMessage(), resetToken));
} else if (authenticationException instanceof UserPasswordNotValidException) {
UserPasswordNotValidException expiredException = (UserPasswordNotValidException) authenticationException;
JacksonUtil.writeValue(response.getWriter(), ThingsboardCredentialsViolationResponse.of(expiredException.getMessage()));
} else { } else {
JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
} }

7
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -271,9 +271,8 @@ public class ThingsboardInstallService {
case "3.6.1": case "3.6.1":
log.info("Upgrading ThingsBoard from version 3.6.1 to 3.6.2 ..."); log.info("Upgrading ThingsBoard from version 3.6.1 to 3.6.2 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.6.1"); databaseEntitiesUpgradeService.upgradeDatabase("3.6.1");
installScripts.loadSystemImages();
if (!getEnv("SKIP_IMAGES_MIGRATION", false)) { if (!getEnv("SKIP_IMAGES_MIGRATION", false)) {
installScripts.updateImages(); installScripts.setUpdateImages(true);
} else { } else {
log.info("Skipping images migration. Run the upgrade with fromVersion as '3.6.2-images' to migrate"); log.info("Skipping images migration. Run the upgrade with fromVersion as '3.6.2-images' to migrate");
} }
@ -288,6 +287,10 @@ public class ThingsboardInstallService {
dataUpdateService.upgradeRuleNodes(); dataUpdateService.upgradeRuleNodes();
systemDataLoaderService.loadSystemWidgets(); systemDataLoaderService.loadSystemWidgets();
installScripts.loadSystemLwm2mResources(); installScripts.loadSystemLwm2mResources();
installScripts.loadSystemImages();
if (installScripts.isUpdateImages()) {
installScripts.updateImages();
}
} }
log.info("Upgrade finished successfully!"); log.info("Upgrade finished successfully!");

4
application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java

@ -16,6 +16,8 @@
package org.thingsboard.server.service.install; package org.thingsboard.server.service.install;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -113,6 +115,8 @@ public class InstallScripts {
@Autowired @Autowired
private ImagesUpdater imagesUpdater; private ImagesUpdater imagesUpdater;
@Getter @Setter
private boolean updateImages = false;
Path getTenantRuleChainsDir() { Path getTenantRuleChainsDir() {
return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR); return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR);

6
application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java

@ -27,6 +27,8 @@ import org.springframework.stereotype.Service;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTINGS_CACHE;
@RequiredArgsConstructor @RequiredArgsConstructor
@Service @Service
@Profile("install") @Profile("install")
@ -86,6 +88,10 @@ public class DefaultCacheCleanupService implements CacheCleanupService {
log.info("Clearing cache to upgrade from version 3.4.4 to 3.5.0"); log.info("Clearing cache to upgrade from version 3.4.4 to 3.5.0");
clearAll(); clearAll();
break; break;
case "3.6.1":
log.info("Clearing cache to upgrade from version 3.6.1 to 3.6.2");
clearCacheByName(SECURITY_SETTINGS_CACHE);
break;
default: default:
//Do nothing, since cache cleanup is optional. //Do nothing, since cache cleanup is optional.
} }

9
application/src/main/java/org/thingsboard/server/service/resource/DefaultTbImageService.java

@ -145,6 +145,15 @@ public class DefaultTbImageService extends AbstractTbEntityService implements Tb
TbImageDeleteResult result = imageService.deleteImage(imageInfo, force); TbImageDeleteResult result = imageService.deleteImage(imageInfo, force);
if (result.isSuccess()) { if (result.isSuccess()) {
notificationEntityService.logEntityAction(tenantId, imageId, imageInfo, ActionType.DELETED, user, imageId.toString()); notificationEntityService.logEntityAction(tenantId, imageId, imageInfo, ActionType.DELETED, user, imageId.toString());
evictETag(new ImageCacheKey(tenantId, imageInfo.getResourceKey(), false));
evictETag(new ImageCacheKey(tenantId, imageInfo.getResourceKey(), true));
clusterService.broadcastToCore(TransportProtos.ToCoreNotificationMsg.newBuilder()
.setResourceCacheInvalidateMsg(TransportProtos.ResourceCacheInvalidateMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setResourceKey(imageInfo.getResourceKey())
.build())
.build());
} }
return result; return result;
} catch (Exception e) { } catch (Exception e) {

3
application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java

@ -23,7 +23,6 @@ import org.thingsboard.server.cache.TbTransactionalCache;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent;
import org.thingsboard.server.common.data.security.model.JwtToken;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import java.util.Optional; import java.util.Optional;
@ -49,7 +48,7 @@ public class DefaultTokenOutdatingService implements TokenOutdatingService {
} }
@Override @Override
public boolean isOutdated(JwtToken token, UserId userId) { public boolean isOutdated(String token, UserId userId) {
Claims claims = tokenFactory.parseTokenClaims(token).getBody(); Claims claims = tokenFactory.parseTokenClaims(token).getBody();
long issueTime = claims.getIssuedAt().getTime(); long issueTime = claims.getIssuedAt().getTime();
String sessionId = claims.get("sessionId", String.class); String sessionId = claims.get("sessionId", String.class);

3
application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java

@ -16,10 +16,9 @@
package org.thingsboard.server.service.security.auth; package org.thingsboard.server.service.security.auth;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.model.JwtToken;
public interface TokenOutdatingService { public interface TokenOutdatingService {
boolean isOutdated(JwtToken token, UserId userId); boolean isOutdated(String token, UserId userId);
} }

16
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java

@ -16,7 +16,9 @@
package org.thingsboard.server.service.security.auth.jwt; package org.thingsboard.server.service.security.auth.jwt;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -37,13 +39,19 @@ public class JwtAuthenticationProvider implements AuthenticationProvider {
@Override @Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException { public Authentication authenticate(Authentication authentication) throws AuthenticationException {
RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials(); RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials();
SecurityUser securityUser = tokenFactory.parseAccessJwtToken(rawAccessToken); SecurityUser securityUser = authenticate(rawAccessToken.getToken());
return new JwtAuthenticationToken(securityUser);
}
if (tokenOutdatingService.isOutdated(rawAccessToken, securityUser.getId())) { public SecurityUser authenticate(String accessToken) throws AuthenticationException {
if (StringUtils.isEmpty(accessToken)) {
throw new BadCredentialsException("Token is invalid");
}
SecurityUser securityUser = tokenFactory.parseAccessJwtToken(accessToken);
if (tokenOutdatingService.isOutdated(accessToken, securityUser.getId())) {
throw new JwtExpiredTokenException("Token is outdated"); throw new JwtExpiredTokenException("Token is outdated");
} }
return securityUser;
return new JwtAuthenticationToken(securityUser);
} }
@Override @Override

4
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java

@ -57,7 +57,7 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide
public Authentication authenticate(Authentication authentication) throws AuthenticationException { public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.notNull(authentication, "No authentication data provided"); Assert.notNull(authentication, "No authentication data provided");
RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials(); RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials();
SecurityUser unsafeUser = tokenFactory.parseRefreshToken(rawAccessToken); SecurityUser unsafeUser = tokenFactory.parseRefreshToken(rawAccessToken.getToken());
UserPrincipal principal = unsafeUser.getUserPrincipal(); UserPrincipal principal = unsafeUser.getUserPrincipal();
SecurityUser securityUser; SecurityUser securityUser;
@ -67,7 +67,7 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide
securityUser = authenticateByPublicId(principal.getValue()); securityUser = authenticateByPublicId(principal.getValue());
} }
securityUser.setSessionId(unsafeUser.getSessionId()); securityUser.setSessionId(unsafeUser.getSessionId());
if (tokenOutdatingService.isOutdated(rawAccessToken, securityUser.getId())) { if (tokenOutdatingService.isOutdated(rawAccessToken.getToken(), securityUser.getId())) {
throw new CredentialsExpiredException("Token is outdated"); throw new CredentialsExpiredException("Token is outdated");
} }

7
application/src/main/java/org/thingsboard/server/service/security/exception/JwtExpiredTokenException.java

@ -16,23 +16,22 @@
package org.thingsboard.server.service.security.exception; package org.thingsboard.server.service.security.exception;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.thingsboard.server.common.data.security.model.JwtToken;
public class JwtExpiredTokenException extends AuthenticationException { public class JwtExpiredTokenException extends AuthenticationException {
private static final long serialVersionUID = -5959543783324224864L; private static final long serialVersionUID = -5959543783324224864L;
private JwtToken token; private String token;
public JwtExpiredTokenException(String msg) { public JwtExpiredTokenException(String msg) {
super(msg); super(msg);
} }
public JwtExpiredTokenException(JwtToken token, String msg, Throwable t) { public JwtExpiredTokenException(String token, String msg, Throwable t) {
super(msg, t); super(msg, t);
this.token = token; this.token = token;
} }
public String token() { public String token() {
return this.token.getToken(); return this.token;
} }
} }

26
application/src/main/java/org/thingsboard/server/service/security/exception/UserPasswordNotValidException.java

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2023 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.service.security.exception;
import org.springframework.security.core.AuthenticationException;
public class UserPasswordNotValidException extends AuthenticationException {
public UserPasswordNotValidException(String msg) {
super(msg);
}
}

12
application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java

@ -93,8 +93,8 @@ public class JwtTokenFactory {
return new AccessJwtToken(token); return new AccessJwtToken(token);
} }
public SecurityUser parseAccessJwtToken(RawAccessJwtToken rawAccessToken) { public SecurityUser parseAccessJwtToken(String token) {
Jws<Claims> jwsClaims = parseTokenClaims(rawAccessToken); Jws<Claims> jwsClaims = parseTokenClaims(token);
Claims claims = jwsClaims.getBody(); Claims claims = jwsClaims.getBody();
String subject = claims.getSubject(); String subject = claims.getSubject();
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@ -145,8 +145,8 @@ public class JwtTokenFactory {
return new AccessJwtToken(token); return new AccessJwtToken(token);
} }
public SecurityUser parseRefreshToken(RawAccessJwtToken rawAccessToken) { public SecurityUser parseRefreshToken(String token) {
Jws<Claims> jwsClaims = parseTokenClaims(rawAccessToken); Jws<Claims> jwsClaims = parseTokenClaims(token);
Claims claims = jwsClaims.getBody(); Claims claims = jwsClaims.getBody();
String subject = claims.getSubject(); String subject = claims.getSubject();
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@ -200,11 +200,11 @@ public class JwtTokenFactory {
.signWith(SignatureAlgorithm.HS512, jwtSettingsService.getJwtSettings().getTokenSigningKey()); .signWith(SignatureAlgorithm.HS512, jwtSettingsService.getJwtSettings().getTokenSigningKey());
} }
public Jws<Claims> parseTokenClaims(JwtToken token) { public Jws<Claims> parseTokenClaims(String token) {
try { try {
return Jwts.parser() return Jwts.parser()
.setSigningKey(jwtSettingsService.getJwtSettings().getTokenSigningKey()) .setSigningKey(jwtSettingsService.getJwtSettings().getTokenSigningKey())
.parseClaimsJws(token.getToken()); .parseClaimsJws(token);
} catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException ex) { } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException ex) {
log.debug("Invalid JWT Token", ex); log.debug("Invalid JWT Token", ex);
throw new BadCredentialsException("Invalid JWT token: ", ex); throw new BadCredentialsException("Invalid JWT token: ", ex);

55
application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java

@ -57,6 +57,7 @@ import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.user.UserServiceImpl; import org.thingsboard.server.dao.user.UserServiceImpl;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; import org.thingsboard.server.service.security.exception.UserPasswordExpiredException;
import org.thingsboard.server.service.security.exception.UserPasswordNotValidException;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.utils.MiscUtils; import org.thingsboard.server.utils.MiscUtils;
import ua_parser.Client; import ua_parser.Client;
@ -107,6 +108,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
securitySettings = new SecuritySettings(); securitySettings = new SecuritySettings();
securitySettings.setPasswordPolicy(new UserPasswordPolicy()); securitySettings.setPasswordPolicy(new UserPasswordPolicy());
securitySettings.getPasswordPolicy().setMinimumLength(6); securitySettings.getPasswordPolicy().setMinimumLength(6);
securitySettings.getPasswordPolicy().setMaximumLength(72);
} }
return securitySettings; return securitySettings;
} }
@ -131,9 +133,19 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
@Override @Override
public void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String username, String password) throws AuthenticationException { public void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String username, String password) throws AuthenticationException {
SecuritySettings securitySettings = self.getSecuritySettings(tenantId);
UserPasswordPolicy passwordPolicy = securitySettings.getPasswordPolicy();
if (!tenantId.isSysTenantId() && Boolean.TRUE.equals(passwordPolicy.getForceUserToResetPasswordIfNotValid())) {
try {
validatePasswordByPolicy(password, passwordPolicy);
} catch (DataValidationException e) {
throw new UserPasswordNotValidException("The entered password violates our policies. If this is your real password, please reset it.");
}
}
if (!encoder.matches(password, userCredentials.getPassword())) { if (!encoder.matches(password, userCredentials.getPassword())) {
int failedLoginAttempts = userService.increaseFailedLoginAttempts(tenantId, userCredentials.getUserId()); int failedLoginAttempts = userService.increaseFailedLoginAttempts(tenantId, userCredentials.getUserId());
SecuritySettings securitySettings = self.getSecuritySettings(tenantId);
if (securitySettings.getMaxFailedLoginAttempts() != null && securitySettings.getMaxFailedLoginAttempts() > 0) { if (securitySettings.getMaxFailedLoginAttempts() != null && securitySettings.getMaxFailedLoginAttempts() > 0) {
if (failedLoginAttempts > securitySettings.getMaxFailedLoginAttempts() && userCredentials.isEnabled()) { if (failedLoginAttempts > securitySettings.getMaxFailedLoginAttempts() && userCredentials.isEnabled()) {
lockAccount(userCredentials.getUserId(), username, securitySettings.getUserLockoutNotificationEmail(), securitySettings.getMaxFailedLoginAttempts()); lockAccount(userCredentials.getUserId(), username, securitySettings.getUserLockoutNotificationEmail(), securitySettings.getMaxFailedLoginAttempts());
@ -149,7 +161,6 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
userService.resetFailedLoginAttempts(tenantId, userCredentials.getUserId()); userService.resetFailedLoginAttempts(tenantId, userCredentials.getUserId());
SecuritySettings securitySettings = self.getSecuritySettings(tenantId);
if (isPositiveInteger(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays())) { if (isPositiveInteger(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays())) {
if ((userCredentials.getCreatedTime() if ((userCredentials.getCreatedTime()
+ TimeUnit.DAYS.toMillis(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays())) + TimeUnit.DAYS.toMillis(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays()))
@ -199,8 +210,31 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
SecuritySettings securitySettings = self.getSecuritySettings(tenantId); SecuritySettings securitySettings = self.getSecuritySettings(tenantId);
UserPasswordPolicy passwordPolicy = securitySettings.getPasswordPolicy(); UserPasswordPolicy passwordPolicy = securitySettings.getPasswordPolicy();
validatePasswordByPolicy(password, passwordPolicy);
if (userCredentials != null && isPositiveInteger(passwordPolicy.getPasswordReuseFrequencyDays())) {
long passwordReuseFrequencyTs = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(passwordPolicy.getPasswordReuseFrequencyDays());
JsonNode additionalInfo = userCredentials.getAdditionalInfo();
if (additionalInfo instanceof ObjectNode && additionalInfo.has(UserServiceImpl.USER_PASSWORD_HISTORY)) {
JsonNode userPasswordHistoryJson = additionalInfo.get(UserServiceImpl.USER_PASSWORD_HISTORY);
Map<String, String> userPasswordHistoryMap = JacksonUtil.convertValue(userPasswordHistoryJson, new TypeReference<>() {});
for (Map.Entry<String, String> entry : userPasswordHistoryMap.entrySet()) {
if (encoder.matches(password, entry.getValue()) && Long.parseLong(entry.getKey()) > passwordReuseFrequencyTs) {
throw new DataValidationException("Password was already used for the last " + passwordPolicy.getPasswordReuseFrequencyDays() + " days");
}
}
}
}
}
private void validatePasswordByPolicy(String password, UserPasswordPolicy passwordPolicy) {
List<Rule> passwordRules = new ArrayList<>(); List<Rule> passwordRules = new ArrayList<>();
passwordRules.add(new LengthRule(passwordPolicy.getMinimumLength(), Integer.MAX_VALUE));
Integer maximumLength = passwordPolicy.getMaximumLength();
Integer minLengthBound = passwordPolicy.getMinimumLength();
int maxLengthBound = (maximumLength != null && maximumLength > passwordPolicy.getMinimumLength()) ? maximumLength : Integer.MAX_VALUE;
passwordRules.add(new LengthRule(minLengthBound, maxLengthBound));
if (isPositiveInteger(passwordPolicy.getMinimumUppercaseLetters())) { if (isPositiveInteger(passwordPolicy.getMinimumUppercaseLetters())) {
passwordRules.add(new CharacterRule(EnglishCharacterData.UpperCase, passwordPolicy.getMinimumUppercaseLetters())); passwordRules.add(new CharacterRule(EnglishCharacterData.UpperCase, passwordPolicy.getMinimumUppercaseLetters()));
} }
@ -223,21 +257,6 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
String message = String.join("\n", validator.getMessages(result)); String message = String.join("\n", validator.getMessages(result));
throw new DataValidationException(message); throw new DataValidationException(message);
} }
if (userCredentials != null && isPositiveInteger(passwordPolicy.getPasswordReuseFrequencyDays())) {
long passwordReuseFrequencyTs = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(passwordPolicy.getPasswordReuseFrequencyDays());
JsonNode additionalInfo = userCredentials.getAdditionalInfo();
if (additionalInfo instanceof ObjectNode && additionalInfo.has(UserServiceImpl.USER_PASSWORD_HISTORY)) {
JsonNode userPasswordHistoryJson = additionalInfo.get(UserServiceImpl.USER_PASSWORD_HISTORY);
Map<String, String> userPasswordHistoryMap = JacksonUtil.convertValue(userPasswordHistoryJson, new TypeReference<>() {});
for (Map.Entry<String, String> entry : userPasswordHistoryMap.entrySet()) {
if (encoder.matches(password, entry.getValue()) && Long.parseLong(entry.getKey()) > passwordReuseFrequencyTs) {
throw new DataValidationException("Password was already used for the last " + passwordPolicy.getPasswordReuseFrequencyDays() + " days");
}
}
}
}
} }
@Override @Override

2
application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java

@ -336,7 +336,7 @@ public abstract class TbAbstractSubCtx<T extends EntityCountQuery> {
public void sendWsMsg(CmdUpdate update) { public void sendWsMsg(CmdUpdate update) {
wsLock.lock(); wsLock.lock();
try { try {
wsService.sendWsMsg(sessionRef.getSessionId(), update); wsService.sendUpdate(sessionRef.getSessionId(), update);
} finally { } finally {
wsLock.unlock(); wsLock.unlock();
} }

3
application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java

@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
@Data @Data
@ -35,6 +36,8 @@ public abstract class TbSubscription<T> {
private final TbSubscriptionType type; private final TbSubscriptionType type;
private final BiConsumer<TbSubscription<T>, T> updateProcessor; private final BiConsumer<TbSubscription<T>, T> updateProcessor;
protected final AtomicInteger sequence = new AtomicInteger();
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

4
application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java

@ -65,8 +65,8 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
private final TbNotificationEntityService entityNotificationService; private final TbNotificationEntityService entityNotificationService;
protected static final List<EntityType> SUPPORTED_ENTITY_TYPES = List.of( protected static final List<EntityType> SUPPORTED_ENTITY_TYPES = List.of(
EntityType.CUSTOMER, EntityType.RULE_CHAIN, EntityType.DASHBOARD, EntityType.CUSTOMER, EntityType.RULE_CHAIN, EntityType.TB_RESOURCE,
EntityType.ASSET_PROFILE, EntityType.ASSET, EntityType.DASHBOARD, EntityType.ASSET_PROFILE, EntityType.ASSET,
EntityType.DEVICE_PROFILE, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.DEVICE,
EntityType.ENTITY_VIEW, EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE, EntityType.ENTITY_VIEW, EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE,
EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE

1
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetProfileExportService.java

@ -34,6 +34,7 @@ public class AssetProfileExportService extends BaseEntityExportService<AssetProf
assetProfile.setDefaultDashboardId(getExternalIdOrElseInternal(ctx, assetProfile.getDefaultDashboardId())); assetProfile.setDefaultDashboardId(getExternalIdOrElseInternal(ctx, assetProfile.getDefaultDashboardId()));
assetProfile.setDefaultRuleChainId(getExternalIdOrElseInternal(ctx, assetProfile.getDefaultRuleChainId())); assetProfile.setDefaultRuleChainId(getExternalIdOrElseInternal(ctx, assetProfile.getDefaultRuleChainId()));
assetProfile.setDefaultEdgeRuleChainId(getExternalIdOrElseInternal(ctx, assetProfile.getDefaultEdgeRuleChainId())); assetProfile.setDefaultEdgeRuleChainId(getExternalIdOrElseInternal(ctx, assetProfile.getDefaultEdgeRuleChainId()));
imageService.inlineImage(assetProfile);
} }
@Override @Override

1
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java

@ -48,6 +48,7 @@ public class DashboardExportService extends BaseEntityExportService<DashboardId,
for (JsonNode widgetConfig : dashboard.getWidgetsConfig()) { for (JsonNode widgetConfig : dashboard.getWidgetsConfig()) {
replaceUuidsRecursively(ctx, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Collections.emptySet(), WIDGET_CONFIG_PROCESSED_FIELDS_PATTERN); replaceUuidsRecursively(ctx, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Collections.emptySet(), WIDGET_CONFIG_PROCESSED_FIELDS_PATTERN);
} }
imageService.inlineImages(dashboard);
} }
@Override @Override

3
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java

@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.sync.ie.AttributeExportData;
import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.relation.RelationDao; import org.thingsboard.server.dao.relation.RelationDao;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.ie.exporting.EntityExportService; import org.thingsboard.server.service.sync.ie.exporting.EntityExportService;
import org.thingsboard.server.service.sync.ie.exporting.ExportableEntitiesService; import org.thingsboard.server.service.sync.ie.exporting.ExportableEntitiesService;
@ -58,6 +59,8 @@ public class DefaultEntityExportService<I extends EntityId, E extends Exportable
private RelationDao relationDao; private RelationDao relationDao;
@Autowired @Autowired
private AttributesService attributesService; private AttributesService attributesService;
@Autowired
protected ImageService imageService;
@Override @Override
public final D getExportData(EntitiesExportCtx<?> ctx, I entityId) throws ThingsboardException { public final D getExportData(EntitiesExportCtx<?> ctx, I entityId) throws ThingsboardException {

1
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java

@ -34,6 +34,7 @@ public class DeviceProfileExportService extends BaseEntityExportService<DevicePr
deviceProfile.setDefaultDashboardId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultDashboardId())); deviceProfile.setDefaultDashboardId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultDashboardId()));
deviceProfile.setDefaultRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultRuleChainId())); deviceProfile.setDefaultRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultRuleChainId()));
deviceProfile.setDefaultEdgeRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultEdgeRuleChainId())); deviceProfile.setDefaultEdgeRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultEdgeRuleChainId()));
imageService.inlineImage(deviceProfile);
} }
@Override @Override

5
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetTypeExportService.java

@ -32,10 +32,11 @@ import java.util.Set;
public class WidgetTypeExportService extends BaseEntityExportService<WidgetTypeId, WidgetTypeDetails, WidgetTypeExportData> { public class WidgetTypeExportService extends BaseEntityExportService<WidgetTypeId, WidgetTypeDetails, WidgetTypeExportData> {
@Override @Override
protected void setRelatedEntities(EntitiesExportCtx<?> ctx, WidgetTypeDetails widgetsBundle, WidgetTypeExportData exportData) { protected void setRelatedEntities(EntitiesExportCtx<?> ctx, WidgetTypeDetails widgetTypeDetails, WidgetTypeExportData exportData) {
if (widgetsBundle.getTenantId() == null || widgetsBundle.getTenantId().isNullUid()) { if (widgetTypeDetails.getTenantId() == null || widgetTypeDetails.getTenantId().isNullUid()) {
throw new IllegalArgumentException("Export of system Widget Type is not allowed"); throw new IllegalArgumentException("Export of system Widget Type is not allowed");
} }
imageService.inlineImages(widgetTypeDetails);
} }
@Override @Override

1
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java

@ -41,6 +41,7 @@ public class WidgetsBundleExportService extends BaseEntityExportService<WidgetsB
if (widgetsBundle.getTenantId() == null || widgetsBundle.getTenantId().isNullUid()) { if (widgetsBundle.getTenantId() == null || widgetsBundle.getTenantId().isNullUid()) {
throw new IllegalArgumentException("Export of system Widget Bundles is not allowed"); throw new IllegalArgumentException("Export of system Widget Bundles is not allowed");
} }
imageService.inlineImage(widgetsBundle);
List<String> fqns = widgetTypeService.findWidgetFqnsByWidgetsBundleId(ctx.getTenantId(), widgetsBundle.getId()); List<String> fqns = widgetTypeService.findWidgetFqnsByWidgetsBundleId(ctx.getTenantId(), widgetsBundle.getId());
exportData.setFqns(fqns); exportData.setFqns(fqns);

33
application/src/main/java/org/thingsboard/server/service/ws/AuthCmd.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2023 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.service.ws;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AuthCmd implements WsCmd {
private int cmdId;
private String token;
@Override
public WsCmdType getType() {
return WsCmdType.AUTH;
}
}

410
application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java

@ -21,8 +21,10 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.CloseStatus;
@ -64,9 +66,6 @@ import org.thingsboard.server.service.subscription.TbEntityDataSubscriptionServi
import org.thingsboard.server.service.subscription.TbLocalSubscriptionService; import org.thingsboard.server.service.subscription.TbLocalSubscriptionService;
import org.thingsboard.server.service.subscription.TbTimeSeriesSubscription; import org.thingsboard.server.service.subscription.TbTimeSeriesSubscription;
import org.thingsboard.server.service.ws.notification.NotificationCommandsHandler; import org.thingsboard.server.service.ws.notification.NotificationCommandsHandler;
import org.thingsboard.server.service.ws.notification.cmd.NotificationCmdsWrapper;
import org.thingsboard.server.service.ws.notification.cmd.WsCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.TelemetryPluginCmdsWrapper;
import org.thingsboard.server.service.ws.telemetry.cmd.v1.AttributesSubscriptionCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v1.AttributesSubscriptionCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v1.GetHistoryCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v1.GetHistoryCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v1.SubscriptionCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v1.SubscriptionCmd;
@ -87,6 +86,7 @@ import javax.annotation.PreDestroy;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -123,7 +123,6 @@ public class DefaultWebSocketService implements WebSocketService {
private static final String FAILED_TO_FETCH_DATA = "Failed to fetch data!"; private static final String FAILED_TO_FETCH_DATA = "Failed to fetch data!";
private static final String FAILED_TO_FETCH_ATTRIBUTES = "Failed to fetch attributes!"; private static final String FAILED_TO_FETCH_ATTRIBUTES = "Failed to fetch attributes!";
private static final String SESSION_META_DATA_NOT_FOUND = "Session meta-data not found!"; private static final String SESSION_META_DATA_NOT_FOUND = "Session meta-data not found!";
private static final String FAILED_TO_PARSE_WS_COMMAND = "Failed to parse websocket command!";
private final ConcurrentMap<String, WsSessionMetaData> wsSessionsMap = new ConcurrentHashMap<>(); private final ConcurrentMap<String, WsSessionMetaData> wsSessionsMap = new ConcurrentHashMap<>();
@ -149,8 +148,7 @@ public class DefaultWebSocketService implements WebSocketService {
private ScheduledExecutorService pingExecutor; private ScheduledExecutorService pingExecutor;
private String serviceId; private String serviceId;
private List<WsCmdListHandler<TelemetryPluginCmdsWrapper, ?>> telemetryCmdsHandlers; private Map<WsCmdType, WsCmdHandler<? extends WsCmd>> cmdsHandlers;
private List<WsCmdHandler<NotificationCmdsWrapper, ? extends WsCmd>> notificationCmdsHandlers;
@PostConstruct @PostConstruct
public void init() { public void init() {
@ -160,26 +158,23 @@ public class DefaultWebSocketService implements WebSocketService {
pingExecutor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("telemetry-web-socket-ping")); pingExecutor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("telemetry-web-socket-ping"));
pingExecutor.scheduleWithFixedDelay(this::sendPing, pingTimeout / NUMBER_OF_PING_ATTEMPTS, pingTimeout / NUMBER_OF_PING_ATTEMPTS, TimeUnit.MILLISECONDS); pingExecutor.scheduleWithFixedDelay(this::sendPing, pingTimeout / NUMBER_OF_PING_ATTEMPTS, pingTimeout / NUMBER_OF_PING_ATTEMPTS, TimeUnit.MILLISECONDS);
telemetryCmdsHandlers = List.of( cmdsHandlers = new EnumMap<>(WsCmdType.class);
newCmdsHandler(TelemetryPluginCmdsWrapper::getAttrSubCmds, this::handleWsAttributesSubscriptionCmd), cmdsHandlers.put(WsCmdType.ATTRIBUTES, newCmdHandler(this::handleWsAttributesSubscriptionCmd));
newCmdsHandler(TelemetryPluginCmdsWrapper::getTsSubCmds, this::handleWsTimeseriesSubscriptionCmd), cmdsHandlers.put(WsCmdType.TIMESERIES, newCmdHandler(this::handleWsTimeseriesSubscriptionCmd));
newCmdsHandler(TelemetryPluginCmdsWrapper::getHistoryCmds, this::handleWsHistoryCmd), cmdsHandlers.put(WsCmdType.TIMESERIES_HISTORY, newCmdHandler(this::handleWsHistoryCmd));
newCmdsHandler(TelemetryPluginCmdsWrapper::getEntityDataCmds, this::handleWsEntityDataCmd), cmdsHandlers.put(WsCmdType.ENTITY_DATA, newCmdHandler(this::handleWsEntityDataCmd));
newCmdsHandler(TelemetryPluginCmdsWrapper::getAlarmDataCmds, this::handleWsAlarmDataCmd), cmdsHandlers.put(WsCmdType.ALARM_DATA, newCmdHandler(this::handleWsAlarmDataCmd));
newCmdsHandler(TelemetryPluginCmdsWrapper::getEntityCountCmds, this::handleWsEntityCountCmd), cmdsHandlers.put(WsCmdType.ENTITY_COUNT, newCmdHandler(this::handleWsEntityCountCmd));
newCmdsHandler(TelemetryPluginCmdsWrapper::getAlarmCountCmds, this::handleWsAlarmCountCmd), cmdsHandlers.put(WsCmdType.ALARM_COUNT, newCmdHandler(this::handleWsAlarmCountCmd));
newCmdsHandler(TelemetryPluginCmdsWrapper::getEntityDataUnsubscribeCmds, this::handleWsDataUnsubscribeCmd), cmdsHandlers.put(WsCmdType.ENTITY_DATA_UNSUBSCRIBE, newCmdHandler(this::handleWsDataUnsubscribeCmd));
newCmdsHandler(TelemetryPluginCmdsWrapper::getAlarmDataUnsubscribeCmds, this::handleWsDataUnsubscribeCmd), cmdsHandlers.put(WsCmdType.ALARM_DATA_UNSUBSCRIBE, newCmdHandler(this::handleWsDataUnsubscribeCmd));
newCmdsHandler(TelemetryPluginCmdsWrapper::getEntityCountUnsubscribeCmds, this::handleWsDataUnsubscribeCmd), cmdsHandlers.put(WsCmdType.ENTITY_COUNT_UNSUBSCRIBE, newCmdHandler(this::handleWsDataUnsubscribeCmd));
newCmdsHandler(TelemetryPluginCmdsWrapper::getAlarmCountUnsubscribeCmds, this::handleWsDataUnsubscribeCmd) cmdsHandlers.put(WsCmdType.ALARM_COUNT_UNSUBSCRIBE, newCmdHandler(this::handleWsDataUnsubscribeCmd));
); cmdsHandlers.put(WsCmdType.NOTIFICATIONS, newCmdHandler(notificationCmdsHandler::handleUnreadNotificationsSubCmd));
notificationCmdsHandlers = List.of( cmdsHandlers.put(WsCmdType.NOTIFICATIONS_COUNT, newCmdHandler(notificationCmdsHandler::handleUnreadNotificationsCountSubCmd));
newCmdHandler(NotificationCmdsWrapper::getUnreadSubCmd, notificationCmdsHandler::handleUnreadNotificationsSubCmd), cmdsHandlers.put(WsCmdType.MARK_NOTIFICATIONS_AS_READ, newCmdHandler(notificationCmdsHandler::handleMarkAsReadCmd));
newCmdHandler(NotificationCmdsWrapper::getUnreadCountSubCmd, notificationCmdsHandler::handleUnreadNotificationsCountSubCmd), cmdsHandlers.put(WsCmdType.MARK_ALL_NOTIFICATIONS_AS_READ, newCmdHandler(notificationCmdsHandler::handleMarkAllAsReadCmd));
newCmdHandler(NotificationCmdsWrapper::getMarkAsReadCmd, notificationCmdsHandler::handleMarkAsReadCmd), cmdsHandlers.put(WsCmdType.NOTIFICATIONS_UNSUBSCRIBE, newCmdHandler(notificationCmdsHandler::handleUnsubCmd));
newCmdHandler(NotificationCmdsWrapper::getMarkAllAsReadCmd, notificationCmdsHandler::handleMarkAllAsReadCmd),
newCmdHandler(NotificationCmdsWrapper::getUnsubCmd, notificationCmdsHandler::handleUnsubCmd)
);
} }
@PreDestroy @PreDestroy
@ -194,7 +189,7 @@ public class DefaultWebSocketService implements WebSocketService {
} }
@Override @Override
public void handleWebSocketSessionEvent(WebSocketSessionRef sessionRef, SessionEvent event) { public void handleSessionEvent(WebSocketSessionRef sessionRef, SessionEvent event) {
String sessionId = sessionRef.getSessionId(); String sessionId = sessionRef.getSessionId();
log.debug(PROCESSING_MSG, sessionId, event); log.debug(PROCESSING_MSG, sessionId, event);
switch (event.getEventType()) { switch (event.getEventType()) {
@ -214,120 +209,75 @@ public class DefaultWebSocketService implements WebSocketService {
} }
@Override @Override
public void handleWebSocketMsg(WebSocketSessionRef sessionRef, String msg) { public void handleCommands(WebSocketSessionRef sessionRef, WsCommandsWrapper commandsWrapper) {
if (log.isTraceEnabled()) { if (commandsWrapper == null || CollectionUtils.isEmpty(commandsWrapper.getCmds())) {
log.trace("[{}] Processing: {}", sessionRef.getSessionId(), msg);
}
try {
switch (sessionRef.getSessionType()) {
case TELEMETRY:
processTelemetryCmds(sessionRef, msg);
break;
case NOTIFICATIONS:
processNotificationCmds(sessionRef, msg);
break;
}
} catch (IOException e) {
log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e);
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(UNKNOWN_SUBSCRIPTION_ID, SubscriptionErrorCode.BAD_REQUEST, FAILED_TO_PARSE_WS_COMMAND));
}
}
private void processTelemetryCmds(WebSocketSessionRef sessionRef, String msg) throws JsonProcessingException {
TelemetryPluginCmdsWrapper cmdsWrapper = JacksonUtil.fromString(msg, TelemetryPluginCmdsWrapper.class);
if (cmdsWrapper == null) {
return; return;
} }
for (WsCmdListHandler<TelemetryPluginCmdsWrapper, ?> cmdHandler : telemetryCmdsHandlers) { String sessionId = sessionRef.getSessionId();
List<?> cmds = cmdHandler.extractCmds(cmdsWrapper); if (!validateSessionMetadata(sessionRef, UNKNOWN_SUBSCRIPTION_ID, sessionId)) {
if (cmds != null) { return;
cmdHandler.handle(sessionRef, cmds);
}
} }
}
private void processNotificationCmds(WebSocketSessionRef sessionRef, String msg) throws IOException { for (WsCmd cmd : commandsWrapper.getCmds()) {
NotificationCmdsWrapper cmdsWrapper = JacksonUtil.fromString(msg, NotificationCmdsWrapper.class); log.debug("[{}][{}][{}] Processing cmd: {}", sessionId, cmd.getType(), cmd.getCmdId(), cmd);
for (WsCmdHandler<NotificationCmdsWrapper, ? extends WsCmd> cmdHandler : notificationCmdsHandlers) { try {
WsCmd cmd = cmdHandler.extractCmd(cmdsWrapper); Optional.ofNullable(cmdsHandlers.get(cmd.getType()))
if (cmd != null) { .ifPresent(cmdHandler -> cmdHandler.handle(sessionRef, cmd));
String sessionId = sessionRef.getSessionId(); } catch (Exception e) {
if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)) { log.error("[sessionId: {}, tenantId: {}, userId: {}] Failed to handle WS cmd: {}", sessionId,
try { sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), cmd, e);
cmdHandler.handle(sessionRef, cmd);
} catch (Exception e) {
log.error("[sessionId: {}, tenantId: {}, userId: {}] Failed to handle WS cmd: {}", sessionId,
sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), cmd, e);
}
}
} }
} }
} }
private void handleWsEntityDataCmd(WebSocketSessionRef sessionRef, EntityDataCmd cmd) { private void handleWsEntityDataCmd(WebSocketSessionRef sessionRef, EntityDataCmd cmd) {
String sessionId = sessionRef.getSessionId(); if (validateSubscriptionCmd(sessionRef, cmd)) {
log.debug("[{}] Processing: {}", sessionId, cmd);
if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)
&& validateSubscriptionCmd(sessionRef, cmd)) {
entityDataSubService.handleCmd(sessionRef, cmd); entityDataSubService.handleCmd(sessionRef, cmd);
} }
} }
private void handleWsEntityCountCmd(WebSocketSessionRef sessionRef, EntityCountCmd cmd) { private void handleWsEntityCountCmd(WebSocketSessionRef sessionRef, EntityCountCmd cmd) {
String sessionId = sessionRef.getSessionId(); if (validateSubscriptionCmd(sessionRef, cmd)) {
log.debug("[{}] Processing: {}", sessionId, cmd);
if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)
&& validateSubscriptionCmd(sessionRef, cmd)) {
entityDataSubService.handleCmd(sessionRef, cmd); entityDataSubService.handleCmd(sessionRef, cmd);
} }
} }
private void handleWsAlarmDataCmd(WebSocketSessionRef sessionRef, AlarmDataCmd cmd) { private void handleWsAlarmDataCmd(WebSocketSessionRef sessionRef, AlarmDataCmd cmd) {
String sessionId = sessionRef.getSessionId(); if (validateSubscriptionCmd(sessionRef, cmd)) {
log.debug("[{}] Processing: {}", sessionId, cmd);
if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)
&& validateSubscriptionCmd(sessionRef, cmd)) {
entityDataSubService.handleCmd(sessionRef, cmd); entityDataSubService.handleCmd(sessionRef, cmd);
} }
} }
private void handleWsDataUnsubscribeCmd(WebSocketSessionRef sessionRef, UnsubscribeCmd cmd) { private void handleWsDataUnsubscribeCmd(WebSocketSessionRef sessionRef, UnsubscribeCmd cmd) {
String sessionId = sessionRef.getSessionId(); entityDataSubService.cancelSubscription(sessionRef.getSessionId(), cmd);
log.debug("[{}] Processing: {}", sessionId, cmd);
if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)) {
entityDataSubService.cancelSubscription(sessionRef.getSessionId(), cmd);
}
} }
private void handleWsAlarmCountCmd(WebSocketSessionRef sessionRef, AlarmCountCmd cmd) { private void handleWsAlarmCountCmd(WebSocketSessionRef sessionRef, AlarmCountCmd cmd) {
String sessionId = sessionRef.getSessionId(); if (validateCmd(sessionRef, cmd)) {
log.debug("[{}] Processing: {}", sessionId, cmd);
if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)
&& validateSubscriptionCmd(sessionRef, cmd)) {
entityDataSubService.handleCmd(sessionRef, cmd); entityDataSubService.handleCmd(sessionRef, cmd);
} }
} }
@Override @Override
public void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate update) { public void sendUpdate(String sessionId, TelemetrySubscriptionUpdate update) {
sendWsMsg(sessionId, update.getSubscriptionId(), update); sendUpdate(sessionId, update.getSubscriptionId(), update);
}
@Override
public void sendUpdate(String sessionId, CmdUpdate update) {
sendUpdate(sessionId, update.getCmdId(), update);
} }
@Override @Override
public void sendWsMsg(String sessionId, CmdUpdate update) { public void sendError(WebSocketSessionRef sessionRef, int subId, SubscriptionErrorCode errorCode, String errorMsg) {
sendWsMsg(sessionId, update.getCmdId(), update); TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(subId, errorCode, errorMsg);
sendUpdate(sessionRef, update);
} }
private <T> void sendWsMsg(String sessionId, int cmdId, T update) { private <T> void sendUpdate(String sessionId, int cmdId, T update) {
WsSessionMetaData md = wsSessionsMap.get(sessionId); WsSessionMetaData md = wsSessionsMap.get(sessionId);
if (md != null) { if (md != null) {
sendWsMsg(md.getSessionRef(), cmdId, update); sendUpdate(md.getSessionRef(), cmdId, update);
} }
} }
@ -455,21 +405,17 @@ public class DefaultWebSocketService implements WebSocketService {
} }
String sessionId = sessionRef.getSessionId(); String sessionId = sessionRef.getSessionId();
log.debug("[{}] Processing: {}", sessionId, cmd); if (cmd.isUnsubscribe()) {
unsubscribe(sessionRef, cmd, sessionId);
if (validateSessionMetadata(sessionRef, cmd, sessionId)) { } else if (validateSubscriptionCmd(sessionRef, cmd)) {
if (cmd.isUnsubscribe()) { EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId());
unsubscribe(sessionRef, cmd, sessionId); log.debug("[{}] fetching latest attributes ({}) values for device: {}", sessionId, cmd.getKeys(), entityId);
} else if (validateSubscriptionCmd(sessionRef, cmd)) { Optional<Set<String>> keysOptional = getKeys(cmd);
EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId()); if (keysOptional.isPresent()) {
log.debug("[{}] fetching latest attributes ({}) values for device: {}", sessionId, cmd.getKeys(), entityId); List<String> keys = new ArrayList<>(keysOptional.get());
Optional<Set<String>> keysOptional = getKeys(cmd); handleWsAttributesSubscriptionByKeys(sessionRef, cmd, sessionId, entityId, keys);
if (keysOptional.isPresent()) { } else {
List<String> keys = new ArrayList<>(keysOptional.get()); handleWsAttributesSubscription(sessionRef, cmd, sessionId, entityId);
handleWsAttributesSubscriptionByKeys(sessionRef, cmd, sessionId, entityId, keys);
} else {
handleWsAttributesSubscription(sessionRef, cmd, sessionId, entityId);
}
} }
} }
} }
@ -503,7 +449,7 @@ public class DefaultWebSocketService implements WebSocketService {
.updateProcessor((subscription, update) -> { .updateProcessor((subscription, update) -> {
subLock.lock(); subLock.lock();
try { try {
sendWsMsg(subscription.getSessionId(), update); sendUpdate(subscription.getSessionId(), update);
} finally { } finally {
subLock.unlock(); subLock.unlock();
} }
@ -511,9 +457,9 @@ public class DefaultWebSocketService implements WebSocketService {
.build(); .build();
subLock.lock(); subLock.lock();
try{ try {
oldSubService.addSubscription(sub); oldSubService.addSubscription(sub);
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData)); sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData));
} finally { } finally {
subLock.unlock(); subLock.unlock();
} }
@ -531,7 +477,7 @@ public class DefaultWebSocketService implements WebSocketService {
update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
FAILED_TO_FETCH_ATTRIBUTES); FAILED_TO_FETCH_ATTRIBUTES);
} }
sendWsMsg(sessionRef, update); sendUpdate(sessionRef, update);
} }
}; };
@ -543,27 +489,15 @@ public class DefaultWebSocketService implements WebSocketService {
} }
private void handleWsHistoryCmd(WebSocketSessionRef sessionRef, GetHistoryCmd cmd) { private void handleWsHistoryCmd(WebSocketSessionRef sessionRef, GetHistoryCmd cmd) {
String sessionId = sessionRef.getSessionId(); if (!validateCmd(sessionRef, cmd, () -> {
WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId); if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty() || cmd.getEntityType() == null || cmd.getEntityType().isEmpty()) {
if (sessionMD == null) { throw new IllegalArgumentException("Device id is empty!");
log.warn("[{}] Session meta data not found. ", sessionId); }
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, if (cmd.getKeys() == null || cmd.getKeys().isEmpty()) {
SESSION_META_DATA_NOT_FOUND); throw new IllegalArgumentException("Keys are empty!");
sendWsMsg(sessionRef, update); }
return; })) return;
}
if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty() || cmd.getEntityType() == null || cmd.getEntityType().isEmpty()) {
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
"Device id is empty!");
sendWsMsg(sessionRef, update);
return;
}
if (cmd.getKeys() == null || cmd.getKeys().isEmpty()) {
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
"Keys are empty!");
sendWsMsg(sessionRef, update);
return;
}
EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId()); EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId());
List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
List<ReadTsKvQuery> queries = keys.stream().map(key -> new BaseReadTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))) List<ReadTsKvQuery> queries = keys.stream().map(key -> new BaseReadTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg())))
@ -572,7 +506,7 @@ public class DefaultWebSocketService implements WebSocketService {
FutureCallback<List<TsKvEntry>> callback = new FutureCallback<List<TsKvEntry>>() { FutureCallback<List<TsKvEntry>> callback = new FutureCallback<List<TsKvEntry>>() {
@Override @Override
public void onSuccess(List<TsKvEntry> data) { public void onSuccess(List<TsKvEntry> data) {
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data)); sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data));
} }
@Override @Override
@ -585,7 +519,7 @@ public class DefaultWebSocketService implements WebSocketService {
update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
FAILED_TO_FETCH_DATA); FAILED_TO_FETCH_DATA);
} }
sendWsMsg(sessionRef, update); sendUpdate(sessionRef, update);
} }
}; };
accessValidator.validate(sessionRef.getSecurityCtx(), Operation.READ_TELEMETRY, entityId, accessValidator.validate(sessionRef.getSecurityCtx(), Operation.READ_TELEMETRY, entityId,
@ -620,7 +554,7 @@ public class DefaultWebSocketService implements WebSocketService {
.updateProcessor((subscription, update) -> { .updateProcessor((subscription, update) -> {
subLock.lock(); subLock.lock();
try { try {
sendWsMsg(subscription.getSessionId(), update); sendUpdate(subscription.getSessionId(), update);
} finally { } finally {
subLock.unlock(); subLock.unlock();
} }
@ -631,7 +565,7 @@ public class DefaultWebSocketService implements WebSocketService {
subLock.lock(); subLock.lock();
try { try {
oldSubService.addSubscription(sub); oldSubService.addSubscription(sub);
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData)); sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData));
} finally { } finally {
subLock.unlock(); subLock.unlock();
} }
@ -640,9 +574,7 @@ public class DefaultWebSocketService implements WebSocketService {
@Override @Override
public void onFailure(Throwable e) { public void onFailure(Throwable e) {
log.error(FAILED_TO_FETCH_ATTRIBUTES, e); log.error(FAILED_TO_FETCH_ATTRIBUTES, e);
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, sendError(sessionRef, cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, FAILED_TO_FETCH_ATTRIBUTES);
FAILED_TO_FETCH_ATTRIBUTES);
sendWsMsg(sessionRef, update);
} }
}; };
@ -660,20 +592,16 @@ public class DefaultWebSocketService implements WebSocketService {
} }
String sessionId = sessionRef.getSessionId(); String sessionId = sessionRef.getSessionId();
log.debug("[{}] Processing: {}", sessionId, cmd); if (cmd.isUnsubscribe()) {
unsubscribe(sessionRef, cmd, sessionId);
if (validateSessionMetadata(sessionRef, cmd, sessionId)) { } else if (validateSubscriptionCmd(sessionRef, cmd)) {
if (cmd.isUnsubscribe()) { EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId());
unsubscribe(sessionRef, cmd, sessionId); Optional<Set<String>> keysOptional = getKeys(cmd);
} else if (validateSubscriptionCmd(sessionRef, cmd)) {
EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId()); if (keysOptional.isPresent()) {
Optional<Set<String>> keysOptional = getKeys(cmd); handleWsTimeSeriesSubscriptionByKeys(sessionRef, cmd, sessionId, entityId);
} else {
if (keysOptional.isPresent()) { handleWsTimeSeriesSubscription(sessionRef, cmd, sessionId, entityId);
handleWsTimeSeriesSubscriptionByKeys(sessionRef, cmd, sessionId, entityId);
} else {
handleWsTimeSeriesSubscription(sessionRef, cmd, sessionId, entityId);
}
} }
} }
} }
@ -721,7 +649,7 @@ public class DefaultWebSocketService implements WebSocketService {
.updateProcessor((subscription, update) -> { .updateProcessor((subscription, update) -> {
subLock.lock(); subLock.lock();
try { try {
sendWsMsg(subscription.getSessionId(), update); sendUpdate(subscription.getSessionId(), update);
} finally { } finally {
subLock.unlock(); subLock.unlock();
} }
@ -734,7 +662,7 @@ public class DefaultWebSocketService implements WebSocketService {
subLock.lock(); subLock.lock();
try { try {
oldSubService.addSubscription(sub); oldSubService.addSubscription(sub);
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data)); sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data));
} finally { } finally {
subLock.unlock(); subLock.unlock();
} }
@ -750,7 +678,7 @@ public class DefaultWebSocketService implements WebSocketService {
update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
FAILED_TO_FETCH_DATA); FAILED_TO_FETCH_DATA);
} }
sendWsMsg(sessionRef, update); sendUpdate(sessionRef, update);
} }
}; };
accessValidator.validate(sessionRef.getSecurityCtx(), Operation.READ_TELEMETRY, entityId, accessValidator.validate(sessionRef.getSecurityCtx(), Operation.READ_TELEMETRY, entityId,
@ -776,7 +704,7 @@ public class DefaultWebSocketService implements WebSocketService {
.updateProcessor((subscription, update) -> { .updateProcessor((subscription, update) -> {
subLock.lock(); subLock.lock();
try { try {
sendWsMsg(subscription.getSessionId(), update); sendUpdate(subscription.getSessionId(), update);
} finally { } finally {
subLock.unlock(); subLock.unlock();
} }
@ -787,9 +715,9 @@ public class DefaultWebSocketService implements WebSocketService {
.build(); .build();
subLock.lock(); subLock.lock();
try{ try {
oldSubService.addSubscription(sub); oldSubService.addSubscription(sub);
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data)); sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data));
} finally { } finally {
subLock.unlock(); subLock.unlock();
} }
@ -802,9 +730,7 @@ public class DefaultWebSocketService implements WebSocketService {
} else { } else {
log.info(FAILED_TO_FETCH_DATA, e); log.info(FAILED_TO_FETCH_DATA, e);
} }
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, sendError(sessionRef, cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, FAILED_TO_FETCH_DATA);
FAILED_TO_FETCH_DATA);
sendWsMsg(sessionRef, update);
} }
}; };
} }
@ -818,95 +744,77 @@ public class DefaultWebSocketService implements WebSocketService {
} }
private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, EntityDataCmd cmd) { private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, EntityDataCmd cmd) {
if (cmd.getCmdId() < 0) { return validateCmd(sessionRef, cmd, () -> {
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, if (cmd.getQuery() == null && !cmd.hasAnyCmd()) {
"Cmd id is negative value!"); throw new IllegalArgumentException("Query is empty!");
sendWsMsg(sessionRef, update); }
return false; });
} else if (cmd.getQuery() == null && !cmd.hasAnyCmd()) {
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
"Query is empty!");
sendWsMsg(sessionRef, update);
return false;
}
return true;
} }
private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, EntityCountCmd cmd) { private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, EntityCountCmd cmd) {
if (cmd.getCmdId() < 0) { return validateCmd(sessionRef, cmd, () -> {
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, if (cmd.getQuery() == null) {
"Cmd id is negative value!"); throw new IllegalArgumentException("Query is empty!");
sendWsMsg(sessionRef, update); }
return false; });
} else if (cmd.getQuery() == null) {
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, "Query is empty!");
sendWsMsg(sessionRef, update);
return false;
}
return true;
} }
private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, AlarmDataCmd cmd) { private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, AlarmDataCmd cmd) {
if (cmd.getCmdId() < 0) { return validateCmd(sessionRef, cmd, () -> {
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, if (cmd.getQuery() == null) {
"Cmd id is negative value!"); throw new IllegalArgumentException("Query is empty!");
sendWsMsg(sessionRef, update); }
return false; });
} else if (cmd.getQuery() == null) {
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
"Query is empty!");
sendWsMsg(sessionRef, update);
return false;
}
return true;
} }
private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, SubscriptionCmd cmd) { private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, SubscriptionCmd cmd) {
if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { return validateCmd(sessionRef, cmd, () -> {
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) {
"Device id is empty!"); throw new IllegalArgumentException("Device id is empty!");
sendWsMsg(sessionRef, update); }
return false; });
}
return true;
}
private boolean validateSessionMetadata(WebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) {
return validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId);
} }
private boolean validateSessionMetadata(WebSocketSessionRef sessionRef, int cmdId, String sessionId) { private boolean validateSessionMetadata(WebSocketSessionRef sessionRef, int cmdId, String sessionId) {
WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId); WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId);
if (sessionMD == null) { if (sessionMD == null) {
log.warn("[{}] Session meta data not found. ", sessionId); log.warn("[{}] Session meta data not found. ", sessionId);
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmdId, SubscriptionErrorCode.INTERNAL_ERROR, sendError(sessionRef, cmdId, SubscriptionErrorCode.INTERNAL_ERROR, SESSION_META_DATA_NOT_FOUND);
SESSION_META_DATA_NOT_FOUND);
sendWsMsg(sessionRef, update);
return false; return false;
} else { } else {
return true; return true;
} }
} }
private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, AlarmCountCmd cmd) { private boolean validateCmd(WebSocketSessionRef sessionRef, WsCmd cmd) {
return validateCmd(sessionRef, cmd, null);
}
private <C extends WsCmd> boolean validateCmd(WebSocketSessionRef sessionRef, C cmd, Runnable validator) {
if (cmd.getCmdId() < 0) { if (cmd.getCmdId() < 0) {
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, sendError(sessionRef, cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, "Cmd id is negative value!");
"Cmd id is negative value!"); return false;
sendWsMsg(sessionRef, update); }
try {
if (validator != null) {
validator.run();
}
} catch (Exception e) {
sendError(sessionRef, cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, e.getMessage());
return false; return false;
} }
return true; return true;
} }
private void sendWsMsg(WebSocketSessionRef sessionRef, EntityDataUpdate update) { private void sendUpdate(WebSocketSessionRef sessionRef, EntityDataUpdate update) {
sendWsMsg(sessionRef, update.getCmdId(), update); sendUpdate(sessionRef, update.getCmdId(), update);
} }
private void sendWsMsg(WebSocketSessionRef sessionRef, TelemetrySubscriptionUpdate update) { private void sendUpdate(WebSocketSessionRef sessionRef, TelemetrySubscriptionUpdate update) {
sendWsMsg(sessionRef, update.getSubscriptionId(), update); sendUpdate(sessionRef, update.getSubscriptionId(), update);
} }
private void sendWsMsg(WebSocketSessionRef sessionRef, int cmdId, Object update) { private void sendUpdate(WebSocketSessionRef sessionRef, int cmdId, Object update) {
try { try {
String msg = JacksonUtil.OBJECT_MAPPER.writeValueAsString(update); String msg = JacksonUtil.OBJECT_MAPPER.writeValueAsString(update);
executor.submit(() -> { executor.submit(() -> {
@ -1055,47 +963,19 @@ public class DefaultWebSocketService implements WebSocketService {
.map(TenantProfile::getDefaultProfileConfiguration).orElse(null); .map(TenantProfile::getDefaultProfileConfiguration).orElse(null);
} }
public static <C extends WsCmd> WsCmdHandler<C> newCmdHandler(BiConsumer<WebSocketSessionRef, C> handler) {
public static <W, C> WsCmdHandler<W, C> newCmdHandler(java.util.function.Function<W, C> cmdExtractor, return new WsCmdHandler<>(handler);
BiConsumer<WebSocketSessionRef, C> handler) {
return new WsCmdHandler<>(cmdExtractor, handler);
}
public static <W, C> WsCmdListHandler<W, C> newCmdsHandler(java.util.function.Function<W, List<C>> cmdsExtractor,
BiConsumer<WebSocketSessionRef, C> handler) {
return new WsCmdListHandler<>(cmdsExtractor, handler);
} }
@RequiredArgsConstructor @RequiredArgsConstructor
public static class WsCmdHandler<W, C> { @Getter
private final java.util.function.Function<W, C> cmdExtractor; @SuppressWarnings("unchecked")
private final BiConsumer<WebSocketSessionRef, C> handler; public static class WsCmdHandler<C extends WsCmd> {
protected final BiConsumer<WebSocketSessionRef, C> handler;
public C extractCmd(W cmdsWrapper) {
return cmdExtractor.apply(cmdsWrapper);
}
@SuppressWarnings("unchecked") public void handle(WebSocketSessionRef sessionRef, WsCmd cmd) {
public void handle(WebSocketSessionRef sessionRef, Object cmd) {
handler.accept(sessionRef, (C) cmd); handler.accept(sessionRef, (C) cmd);
} }
} }
@RequiredArgsConstructor
public static class WsCmdListHandler<W, C> {
private final java.util.function.Function<W, List<C>> cmdsExtractor;
private final BiConsumer<WebSocketSessionRef, C> handler;
public List<C> extractCmds(W cmdsWrapper) {
return cmdsExtractor.apply(cmdsWrapper);
}
@SuppressWarnings("unchecked")
public void handle(WebSocketSessionRef sessionRef, List<?> cmds) {
cmds.forEach(cmd -> {
handler.accept(sessionRef, (C) cmd);
});
}
}
} }

11
application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.ws; package org.thingsboard.server.service.ws;
import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.CloseStatus;
import org.thingsboard.server.service.subscription.SubscriptionErrorCode;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
@ -24,13 +25,15 @@ import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpda
*/ */
public interface WebSocketService { public interface WebSocketService {
void handleWebSocketSessionEvent(WebSocketSessionRef sessionRef, SessionEvent sessionEvent); void handleSessionEvent(WebSocketSessionRef sessionRef, SessionEvent sessionEvent);
void handleWebSocketMsg(WebSocketSessionRef sessionRef, String msg); void handleCommands(WebSocketSessionRef sessionRef, WsCommandsWrapper commandsWrapper);
void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate update); void sendUpdate(String sessionId, TelemetrySubscriptionUpdate update);
void sendWsMsg(String sessionId, CmdUpdate update); void sendUpdate(String sessionId, CmdUpdate update);
void sendError(WebSocketSessionRef sessionRef, int subId, SubscriptionErrorCode errorCode, String errorMsg);
void close(String sessionId, CloseStatus status); void close(String sessionId, CloseStatus status);
} }

22
application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java

@ -16,8 +16,7 @@
package org.thingsboard.server.service.ws; package org.thingsboard.server.service.ws;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
@ -27,15 +26,14 @@ import java.util.concurrent.atomic.AtomicInteger;
/** /**
* Created by ashvayka on 27.03.18. * Created by ashvayka on 27.03.18.
*/ */
@RequiredArgsConstructor
@Builder @Builder
@Getter @Data
public class WebSocketSessionRef { public class WebSocketSessionRef {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private final String sessionId; private final String sessionId;
private final SecurityUser securityCtx; private SecurityUser securityCtx;
private final InetSocketAddress localAddress; private final InetSocketAddress localAddress;
private final InetSocketAddress remoteAddress; private final InetSocketAddress remoteAddress;
private final WebSocketSessionType sessionType; private final WebSocketSessionType sessionType;
@ -56,11 +54,13 @@ public class WebSocketSessionRef {
@Override @Override
public String toString() { public String toString() {
return "WebSocketSessionRef{" + String info = "";
"sessionId='" + sessionId + '\'' + if (securityCtx != null) {
", localAddress=" + localAddress + info += "[" + securityCtx.getTenantId() + "]";
", remoteAddress=" + remoteAddress + info += "[" + securityCtx.getId() + "]";
", sessionType=" + sessionType + }
'}'; info += "[" + sessionId + "]";
return info;
} }
} }

18
application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionType.java

@ -15,23 +15,25 @@
*/ */
package org.thingsboard.server.service.ws; package org.thingsboard.server.service.ws;
import lombok.Getter; import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor; import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import java.util.Arrays; import java.util.Arrays;
import java.util.Optional; import java.util.Optional;
@RequiredArgsConstructor @NoArgsConstructor
@Getter @AllArgsConstructor
public enum WebSocketSessionType { public enum WebSocketSessionType {
TELEMETRY("telemetry"), GENERAL(),
NOTIFICATIONS("notifications"); TELEMETRY("telemetry"), // deprecated
NOTIFICATIONS("notifications"); // deprecated
private final String name; private String name;
public static Optional<WebSocketSessionType> forName(String name) { public static Optional<WebSocketSessionType> forName(String name) {
return Arrays.stream(values()) return Arrays.stream(values())
.filter(sessionType -> sessionType.getName().equals(name)) .filter(sessionType -> StringUtils.equals(sessionType.name, name))
.findFirst(); .findFirst();
} }

10
application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/WsCmd.java → application/src/main/java/org/thingsboard/server/service/ws/WsCmd.java

@ -13,8 +13,16 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.service.ws.notification.cmd; package org.thingsboard.server.service.ws;
import com.fasterxml.jackson.annotation.JsonIgnore;
public interface WsCmd { public interface WsCmd {
int getCmdId(); int getCmdId();
@JsonIgnore
WsCmdType getType();
} }

39
application/src/main/java/org/thingsboard/server/service/ws/WsCmdType.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2023 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.service.ws;
public enum WsCmdType {
AUTH,
ATTRIBUTES,
TIMESERIES,
TIMESERIES_HISTORY,
ENTITY_DATA,
ENTITY_COUNT,
ALARM_DATA,
ALARM_COUNT,
NOTIFICATIONS,
NOTIFICATIONS_COUNT,
MARK_NOTIFICATIONS_AS_READ,
MARK_ALL_NOTIFICATIONS_AS_READ,
ALARM_DATA_UNSUBSCRIBE,
ALARM_COUNT_UNSUBSCRIBE,
ENTITY_DATA_UNSUBSCRIBE,
ENTITY_COUNT_UNSUBSCRIBE,
NOTIFICATIONS_UNSUBSCRIBE
}

71
application/src/main/java/org/thingsboard/server/service/ws/WsCommandsWrapper.java

@ -0,0 +1,71 @@
/**
* Copyright © 2016-2023 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.service.ws;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.service.ws.notification.cmd.MarkAllNotificationsAsReadCmd;
import org.thingsboard.server.service.ws.notification.cmd.MarkNotificationsAsReadCmd;
import org.thingsboard.server.service.ws.notification.cmd.NotificationsCountSubCmd;
import org.thingsboard.server.service.ws.notification.cmd.NotificationsSubCmd;
import org.thingsboard.server.service.ws.notification.cmd.NotificationsUnsubCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v1.AttributesSubscriptionCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v1.GetHistoryCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v1.TimeseriesSubscriptionCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUnsubscribeCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataUnsubscribeCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUnsubscribeCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUnsubscribeCmd;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class WsCommandsWrapper {
private AuthCmd authCmd;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@Type(name = "ATTRIBUTES", value = AttributesSubscriptionCmd.class),
@Type(name = "TIMESERIES", value = TimeseriesSubscriptionCmd.class),
@Type(name = "TIMESERIES_HISTORY", value = GetHistoryCmd.class),
@Type(name = "ENTITY_DATA", value = EntityDataCmd.class),
@Type(name = "ENTITY_COUNT", value = EntityCountCmd.class),
@Type(name = "ALARM_DATA", value = AlarmDataCmd.class),
@Type(name = "ALARM_COUNT", value = AlarmCountCmd.class),
@Type(name = "NOTIFICATIONS", value = NotificationsSubCmd.class),
@Type(name = "NOTIFICATIONS_COUNT", value = NotificationsCountSubCmd.class),
@Type(name = "MARK_NOTIFICATIONS_AS_READ", value = MarkNotificationsAsReadCmd.class),
@Type(name = "MARK_ALL_NOTIFICATIONS_AS_READ", value = MarkAllNotificationsAsReadCmd.class),
@Type(name = "ALARM_DATA_UNSUBSCRIBE", value = AlarmDataUnsubscribeCmd.class),
@Type(name = "ALARM_COUNT_UNSUBSCRIBE", value = AlarmCountUnsubscribeCmd.class),
@Type(name = "ENTITY_DATA_UNSUBSCRIBE", value = EntityDataUnsubscribeCmd.class),
@Type(name = "ENTITY_COUNT_UNSUBSCRIBE", value = EntityCountUnsubscribeCmd.class),
@Type(name = "NOTIFICATIONS_UNSUBSCRIBE", value = NotificationsUnsubCmd.class),
})
private List<WsCmd> cmds;
}

2
application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java

@ -245,7 +245,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
private void sendUpdate(String sessionId, CmdUpdate update) { private void sendUpdate(String sessionId, CmdUpdate update) {
log.trace("[{}, cmdId: {}] Sending WS update: {}", sessionId, update.getCmdId(), update); log.trace("[{}, cmdId: {}] Sending WS update: {}", sessionId, update.getCmdId(), update);
wsService.sendWsMsg(sessionId, update); wsService.sendUpdate(sessionId, update);
} }
} }

7
application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/MarkAllNotificationsAsReadCmd.java

@ -18,10 +18,17 @@ package org.thingsboard.server.service.ws.notification.cmd;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.service.ws.WsCmd;
import org.thingsboard.server.service.ws.WsCmdType;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class MarkAllNotificationsAsReadCmd implements WsCmd { public class MarkAllNotificationsAsReadCmd implements WsCmd {
private int cmdId; private int cmdId;
@Override
public WsCmdType getType() {
return WsCmdType.MARK_ALL_NOTIFICATIONS_AS_READ;
}
} }

7
application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/MarkNotificationsAsReadCmd.java

@ -18,6 +18,8 @@ package org.thingsboard.server.service.ws.notification.cmd;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.service.ws.WsCmd;
import org.thingsboard.server.service.ws.WsCmdType;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -28,4 +30,9 @@ import java.util.UUID;
public class MarkNotificationsAsReadCmd implements WsCmd { public class MarkNotificationsAsReadCmd implements WsCmd {
private int cmdId; private int cmdId;
private List<UUID> notifications; private List<UUID> notifications;
@Override
public WsCmdType getType() {
return WsCmdType.MARK_NOTIFICATIONS_AS_READ;
}
} }

19
application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationCmdsWrapper.java

@ -15,9 +15,19 @@
*/ */
package org.thingsboard.server.service.ws.notification.cmd; package org.thingsboard.server.service.ws.notification.cmd;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.service.ws.WsCommandsWrapper;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @deprecated Use {@link WsCommandsWrapper}. This class is left for backward compatibility
* */
@Data @Data
@Deprecated
public class NotificationCmdsWrapper { public class NotificationCmdsWrapper {
private NotificationsCountSubCmd unreadCountSubCmd; private NotificationsCountSubCmd unreadCountSubCmd;
@ -30,4 +40,13 @@ public class NotificationCmdsWrapper {
private NotificationsUnsubCmd unsubCmd; private NotificationsUnsubCmd unsubCmd;
@JsonIgnore
public WsCommandsWrapper toCommonCmdsWrapper() {
return new WsCommandsWrapper(null, Stream.of(
unreadCountSubCmd, unreadSubCmd, markAsReadCmd, markAllAsReadCmd, unsubCmd
)
.filter(Objects::nonNull)
.collect(Collectors.toList()));
}
} }

7
application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsCountSubCmd.java

@ -18,10 +18,17 @@ package org.thingsboard.server.service.ws.notification.cmd;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.service.ws.WsCmd;
import org.thingsboard.server.service.ws.WsCmdType;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class NotificationsCountSubCmd implements WsCmd { public class NotificationsCountSubCmd implements WsCmd {
private int cmdId; private int cmdId;
@Override
public WsCmdType getType() {
return WsCmdType.NOTIFICATIONS_COUNT;
}
} }

7
application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsSubCmd.java

@ -18,6 +18,8 @@ package org.thingsboard.server.service.ws.notification.cmd;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.service.ws.WsCmd;
import org.thingsboard.server.service.ws.WsCmdType;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@ -25,4 +27,9 @@ import lombok.NoArgsConstructor;
public class NotificationsSubCmd implements WsCmd { public class NotificationsSubCmd implements WsCmd {
private int cmdId; private int cmdId;
private int limit; private int limit;
@Override
public WsCmdType getType() {
return WsCmdType.NOTIFICATIONS;
}
} }

7
application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsUnsubCmd.java

@ -18,6 +18,8 @@ package org.thingsboard.server.service.ws.notification.cmd;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.service.ws.WsCmd;
import org.thingsboard.server.service.ws.WsCmdType;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd;
@Data @Data
@ -25,4 +27,9 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd;
@AllArgsConstructor @AllArgsConstructor
public class NotificationsUnsubCmd implements UnsubscribeCmd, WsCmd { public class NotificationsUnsubCmd implements UnsubscribeCmd, WsCmd {
private int cmdId; private int cmdId;
@Override
public WsCmdType getType() {
return WsCmdType.NOTIFICATIONS_UNSUBSCRIBE;
}
} }

5
application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/UnreadNotificationsCountUpdate.java

@ -28,14 +28,17 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdateType;
public class UnreadNotificationsCountUpdate extends CmdUpdate { public class UnreadNotificationsCountUpdate extends CmdUpdate {
private final int totalUnreadCount; private final int totalUnreadCount;
private final int sequenceNumber;
@Builder @Builder
@JsonCreator @JsonCreator
public UnreadNotificationsCountUpdate(@JsonProperty("cmdId") int cmdId, @JsonProperty("errorCode") int errorCode, public UnreadNotificationsCountUpdate(@JsonProperty("cmdId") int cmdId, @JsonProperty("errorCode") int errorCode,
@JsonProperty("errorMsg") String errorMsg, @JsonProperty("errorMsg") String errorMsg,
@JsonProperty("totalUnreadCount") int totalUnreadCount) { @JsonProperty("totalUnreadCount") int totalUnreadCount,
@JsonProperty("sequenceNumber") int sequenceNumber) {
super(cmdId, errorCode, errorMsg); super(cmdId, errorCode, errorMsg);
this.totalUnreadCount = totalUnreadCount; this.totalUnreadCount = totalUnreadCount;
this.sequenceNumber = sequenceNumber;
} }
@Override @Override

5
application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/UnreadNotificationsUpdate.java

@ -33,6 +33,7 @@ public class UnreadNotificationsUpdate extends CmdUpdate {
private final Collection<Notification> notifications; private final Collection<Notification> notifications;
private final Notification update; private final Notification update;
private final int totalUnreadCount; private final int totalUnreadCount;
private final int sequenceNumber;
@Builder @Builder
@JsonCreator @JsonCreator
@ -40,11 +41,13 @@ public class UnreadNotificationsUpdate extends CmdUpdate {
@JsonProperty("errorMsg") String errorMsg, @JsonProperty("errorMsg") String errorMsg,
@JsonProperty("notifications") Collection<Notification> notifications, @JsonProperty("notifications") Collection<Notification> notifications,
@JsonProperty("update") Notification update, @JsonProperty("update") Notification update,
@JsonProperty("totalUnreadCount") int totalUnreadCount) { @JsonProperty("totalUnreadCount") int totalUnreadCount,
@JsonProperty("sequenceNumber") int sequenceNumber) {
super(cmdId, errorCode, errorMsg); super(cmdId, errorCode, errorMsg);
this.notifications = notifications; this.notifications = notifications;
this.update = update; this.update = update;
this.totalUnreadCount = totalUnreadCount; this.totalUnreadCount = totalUnreadCount;
this.sequenceNumber = sequenceNumber;
} }
@Override @Override

1
application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java

@ -41,6 +41,7 @@ public class NotificationsCountSubscription extends TbSubscription<Notifications
return UnreadNotificationsCountUpdate.builder() return UnreadNotificationsCountUpdate.builder()
.cmdId(getSubscriptionId()) .cmdId(getSubscriptionId())
.totalUnreadCount(unreadCounter.get()) .totalUnreadCount(unreadCounter.get())
.sequenceNumber(sequence.incrementAndGet())
.build(); .build();
} }

3
application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java

@ -54,6 +54,7 @@ public class NotificationsSubscription extends TbSubscription<NotificationsSubsc
.cmdId(getSubscriptionId()) .cmdId(getSubscriptionId())
.notifications(getSortedNotifications()) .notifications(getSortedNotifications())
.totalUnreadCount(totalUnreadCounter.get()) .totalUnreadCount(totalUnreadCounter.get())
.sequenceNumber(sequence.incrementAndGet())
.build(); .build();
} }
@ -68,6 +69,7 @@ public class NotificationsSubscription extends TbSubscription<NotificationsSubsc
.cmdId(getSubscriptionId()) .cmdId(getSubscriptionId())
.update(notification) .update(notification)
.totalUnreadCount(totalUnreadCounter.get()) .totalUnreadCount(totalUnreadCounter.get())
.sequenceNumber(sequence.incrementAndGet())
.build(); .build();
} }
@ -75,6 +77,7 @@ public class NotificationsSubscription extends TbSubscription<NotificationsSubsc
return UnreadNotificationsUpdate.builder() return UnreadNotificationsUpdate.builder()
.cmdId(getSubscriptionId()) .cmdId(getSubscriptionId())
.totalUnreadCount(totalUnreadCounter.get()) .totalUnreadCount(totalUnreadCounter.get())
.sequenceNumber(sequence.incrementAndGet())
.build(); .build();
} }

26
application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/TelemetryPluginCmdsWrapper.java → application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/TelemetryCmdsWrapper.java

@ -15,7 +15,9 @@
*/ */
package org.thingsboard.server.service.ws.telemetry.cmd; package org.thingsboard.server.service.ws.telemetry.cmd;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.service.ws.WsCommandsWrapper;
import org.thingsboard.server.service.ws.telemetry.cmd.v1.AttributesSubscriptionCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v1.AttributesSubscriptionCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v1.GetHistoryCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v1.GetHistoryCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v1.TimeseriesSubscriptionCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v1.TimeseriesSubscriptionCmd;
@ -28,13 +30,18 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUnsubscribe
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUnsubscribeCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUnsubscribeCmd;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/** /**
* @author Andrew Shvayka * @deprecated Use {@link WsCommandsWrapper}. This class is left for backward compatibility
*/ * */
@Data @Data
public class TelemetryPluginCmdsWrapper { @Deprecated
public class TelemetryCmdsWrapper {
private List<AttributesSubscriptionCmd> attrSubCmds; private List<AttributesSubscriptionCmd> attrSubCmds;
@ -58,4 +65,17 @@ public class TelemetryPluginCmdsWrapper {
private List<AlarmCountUnsubscribeCmd> alarmCountUnsubscribeCmds; private List<AlarmCountUnsubscribeCmd> alarmCountUnsubscribeCmds;
@JsonIgnore
public WsCommandsWrapper toCommonCmdsWrapper() {
return new WsCommandsWrapper(null, Stream.of(
attrSubCmds, tsSubCmds, historyCmds, entityDataCmds,
entityDataUnsubscribeCmds, alarmDataCmds, alarmDataUnsubscribeCmds,
entityCountCmds, entityCountUnsubscribeCmds,
alarmCountCmds, alarmCountUnsubscribeCmds
)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.collect(Collectors.toList()));
}
} }

6
application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/AttributesSubscriptionCmd.java

@ -16,7 +16,7 @@
package org.thingsboard.server.service.ws.telemetry.cmd.v1; package org.thingsboard.server.service.ws.telemetry.cmd.v1;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.service.ws.telemetry.TelemetryFeature; import org.thingsboard.server.service.ws.WsCmdType;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
@ -25,8 +25,8 @@ import org.thingsboard.server.service.ws.telemetry.TelemetryFeature;
public class AttributesSubscriptionCmd extends SubscriptionCmd { public class AttributesSubscriptionCmd extends SubscriptionCmd {
@Override @Override
public TelemetryFeature getType() { public WsCmdType getType() {
return TelemetryFeature.ATTRIBUTES; return WsCmdType.ATTRIBUTES;
} }
} }

5
application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/GetHistoryCmd.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.ws.telemetry.cmd.v1;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.service.ws.WsCmdType;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
@ -37,4 +38,8 @@ public class GetHistoryCmd implements TelemetryPluginCmd {
private int limit; private int limit;
private String agg; private String agg;
@Override
public WsCmdType getType() {
return WsCmdType.TIMESERIES_HISTORY;
}
} }

2
application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/SubscriptionCmd.java

@ -32,8 +32,6 @@ public abstract class SubscriptionCmd implements TelemetryPluginCmd {
private String scope; private String scope;
private boolean unsubscribe; private boolean unsubscribe;
public abstract TelemetryFeature getType();
@Override @Override
public String toString() { public String toString() {
return "SubscriptionCmd [entityType=" + entityType + ", entityId=" + entityId + ", tags=" + keys + ", unsubscribe=" + unsubscribe + "]"; return "SubscriptionCmd [entityType=" + entityType + ", entityId=" + entityId + ", tags=" + keys + ", unsubscribe=" + unsubscribe + "]";

4
application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/TelemetryPluginCmd.java

@ -15,10 +15,12 @@
*/ */
package org.thingsboard.server.service.ws.telemetry.cmd.v1; package org.thingsboard.server.service.ws.telemetry.cmd.v1;
import org.thingsboard.server.service.ws.WsCmd;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
*/ */
public interface TelemetryPluginCmd { public interface TelemetryPluginCmd extends WsCmd {
int getCmdId(); int getCmdId();

8
application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/TimeseriesSubscriptionCmd.java

@ -17,8 +17,9 @@ package org.thingsboard.server.service.ws.telemetry.cmd.v1;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.service.ws.telemetry.TelemetryFeature; import org.thingsboard.server.service.ws.WsCmdType;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
@ -26,6 +27,7 @@ import org.thingsboard.server.service.ws.telemetry.TelemetryFeature;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Data @Data
@EqualsAndHashCode(callSuper = true)
public class TimeseriesSubscriptionCmd extends SubscriptionCmd { public class TimeseriesSubscriptionCmd extends SubscriptionCmd {
private long startTs; private long startTs;
@ -35,7 +37,7 @@ public class TimeseriesSubscriptionCmd extends SubscriptionCmd {
private String agg; private String agg;
@Override @Override
public TelemetryFeature getType() { public WsCmdType getType() {
return TelemetryFeature.TIMESERIES; return WsCmdType.TIMESERIES;
} }
} }

6
application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmCountCmd.java

@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter; import lombok.Getter;
import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmCountQuery;
import org.thingsboard.server.service.ws.WsCmdType;
public class AlarmCountCmd extends DataCmd { public class AlarmCountCmd extends DataCmd {
@ -31,4 +32,9 @@ public class AlarmCountCmd extends DataCmd {
super(cmdId); super(cmdId);
this.query = query; this.query = query;
} }
@Override
public WsCmdType getType() {
return WsCmdType.ALARM_COUNT;
}
} }

5
application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmCountUnsubscribeCmd.java

@ -16,10 +16,15 @@
package org.thingsboard.server.service.ws.telemetry.cmd.v2; package org.thingsboard.server.service.ws.telemetry.cmd.v2;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.service.ws.WsCmdType;
@Data @Data
public class AlarmCountUnsubscribeCmd implements UnsubscribeCmd { public class AlarmCountUnsubscribeCmd implements UnsubscribeCmd {
private final int cmdId; private final int cmdId;
@Override
public WsCmdType getType() {
return WsCmdType.ALARM_COUNT_UNSUBSCRIBE;
}
} }

6
application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmDataCmd.java

@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter; import lombok.Getter;
import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.service.ws.WsCmdType;
public class AlarmDataCmd extends DataCmd { public class AlarmDataCmd extends DataCmd {
@ -30,4 +31,9 @@ public class AlarmDataCmd extends DataCmd {
super(cmdId); super(cmdId);
this.query = query; this.query = query;
} }
@Override
public WsCmdType getType() {
return WsCmdType.ALARM_DATA;
}
} }

5
application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmDataUnsubscribeCmd.java

@ -16,10 +16,15 @@
package org.thingsboard.server.service.ws.telemetry.cmd.v2; package org.thingsboard.server.service.ws.telemetry.cmd.v2;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.service.ws.WsCmdType;
@Data @Data
public class AlarmDataUnsubscribeCmd implements UnsubscribeCmd { public class AlarmDataUnsubscribeCmd implements UnsubscribeCmd {
private final int cmdId; private final int cmdId;
@Override
public WsCmdType getType() {
return WsCmdType.ALARM_DATA_UNSUBSCRIBE;
}
} }

3
application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/DataCmd.java

@ -17,9 +17,10 @@ package org.thingsboard.server.service.ws.telemetry.cmd.v2;
import lombok.Data; import lombok.Data;
import lombok.Getter; import lombok.Getter;
import org.thingsboard.server.service.ws.WsCmd;
@Data @Data
public class DataCmd { public abstract class DataCmd implements WsCmd {
@Getter @Getter
private final int cmdId; private final int cmdId;

6
application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityCountCmd.java

@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter; import lombok.Getter;
import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.service.ws.WsCmdType;
public class EntityCountCmd extends DataCmd { public class EntityCountCmd extends DataCmd {
@ -31,4 +32,9 @@ public class EntityCountCmd extends DataCmd {
super(cmdId); super(cmdId);
this.query = query; this.query = query;
} }
@Override
public WsCmdType getType() {
return WsCmdType.ENTITY_COUNT;
}
} }

5
application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityCountUnsubscribeCmd.java

@ -16,10 +16,15 @@
package org.thingsboard.server.service.ws.telemetry.cmd.v2; package org.thingsboard.server.service.ws.telemetry.cmd.v2;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.service.ws.WsCmdType;
@Data @Data
public class EntityCountUnsubscribeCmd implements UnsubscribeCmd { public class EntityCountUnsubscribeCmd implements UnsubscribeCmd {
private final int cmdId; private final int cmdId;
@Override
public WsCmdType getType() {
return WsCmdType.ENTITY_COUNT_UNSUBSCRIBE;
}
} }

5
application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityDataCmd.java

@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter; import lombok.Getter;
import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.service.ws.WsCmdType;
public class EntityDataCmd extends DataCmd { public class EntityDataCmd extends DataCmd {
@ -62,4 +63,8 @@ public class EntityDataCmd extends DataCmd {
return historyCmd != null || latestCmd != null || tsCmd != null || aggHistoryCmd != null || aggTsCmd != null; return historyCmd != null || latestCmd != null || tsCmd != null || aggHistoryCmd != null || aggTsCmd != null;
} }
@Override
public WsCmdType getType() {
return WsCmdType.ENTITY_DATA;
}
} }

5
application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityDataUnsubscribeCmd.java

@ -16,10 +16,15 @@
package org.thingsboard.server.service.ws.telemetry.cmd.v2; package org.thingsboard.server.service.ws.telemetry.cmd.v2;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.service.ws.WsCmdType;
@Data @Data
public class EntityDataUnsubscribeCmd implements UnsubscribeCmd { public class EntityDataUnsubscribeCmd implements UnsubscribeCmd {
private final int cmdId; private final int cmdId;
@Override
public WsCmdType getType() {
return WsCmdType.ENTITY_DATA_UNSUBSCRIBE;
}
} }

4
application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/UnsubscribeCmd.java

@ -15,7 +15,9 @@
*/ */
package org.thingsboard.server.service.ws.telemetry.cmd.v2; package org.thingsboard.server.service.ws.telemetry.cmd.v2;
public interface UnsubscribeCmd { import org.thingsboard.server.service.ws.WsCmd;
public interface UnsubscribeCmd extends WsCmd {
int getCmdId(); int getCmdId();

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

@ -76,6 +76,8 @@ server:
max_entities_per_alarm_subscription: "${TB_SERVER_WS_MAX_ENTITIES_PER_ALARM_SUBSCRIPTION:10000}" max_entities_per_alarm_subscription: "${TB_SERVER_WS_MAX_ENTITIES_PER_ALARM_SUBSCRIPTION:10000}"
# Maximum queue size of the websocket updates per session. This restriction prevents infinite updates of WS # Maximum queue size of the websocket updates per session. This restriction prevents infinite updates of WS
max_queue_messages_per_session: "${TB_SERVER_WS_DEFAULT_QUEUE_MESSAGES_PER_SESSION:1000}" max_queue_messages_per_session: "${TB_SERVER_WS_DEFAULT_QUEUE_MESSAGES_PER_SESSION:1000}"
# Maximum time between WS session opening and sending auth command
auth_timeout_ms: "${TB_SERVER_WS_AUTH_TIMEOUT_MS:10000}"
rest: rest:
server_side_rpc: server_side_rpc:
# Minimum value of the server-side RPC timeout. May override value provided in the REST API call. # Minimum value of the server-side RPC timeout. May override value provided in the REST API call.
@ -191,7 +193,7 @@ ui:
# Help parameters # Help parameters
help: help:
# Base URL for UI help assets # Base URL for UI help assets
base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.6.1}" base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.6.2}"
# Database telemetry parameters # Database telemetry parameters
database: database:

13
application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java

@ -52,8 +52,8 @@ public abstract class AbstractControllerTest extends AbstractNotifyEntityTest {
@LocalServerPort @LocalServerPort
protected int wsPort; protected int wsPort;
private volatile TbTestWebSocketClient wsClient; // lazy protected volatile TbTestWebSocketClient wsClient; // lazy
private volatile TbTestWebSocketClient anotherWsClient; // lazy protected volatile TbTestWebSocketClient anotherWsClient; // lazy
public TbTestWebSocketClient getWsClient() { public TbTestWebSocketClient getWsClient() {
if (wsClient == null) { if (wsClient == null) {
@ -101,8 +101,15 @@ public abstract class AbstractControllerTest extends AbstractNotifyEntityTest {
} }
protected TbTestWebSocketClient buildAndConnectWebSocketClient() throws URISyntaxException, InterruptedException { protected TbTestWebSocketClient buildAndConnectWebSocketClient() throws URISyntaxException, InterruptedException {
TbTestWebSocketClient wsClient = new TbTestWebSocketClient(new URI(WS_URL + wsPort + "/api/ws/plugins/telemetry?token=" + token)); return buildAndConnectWebSocketClient("/api/ws");
}
protected TbTestWebSocketClient buildAndConnectWebSocketClient(String path) throws URISyntaxException, InterruptedException {
TbTestWebSocketClient wsClient = new TbTestWebSocketClient(new URI(WS_URL + wsPort + path));
assertThat(wsClient.connectBlocking(TIMEOUT, TimeUnit.SECONDS)).isTrue(); assertThat(wsClient.connectBlocking(TIMEOUT, TimeUnit.SECONDS)).isTrue();
if (!path.contains("token=")) {
wsClient.authenticate(token);
}
return wsClient; return wsClient;
} }

83
application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java

@ -15,19 +15,41 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import org.junit.After;
import org.junit.Test; import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.http.HttpHeaders;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.security.auth.rest.LoginRequest;
import org.thingsboard.server.service.security.model.ChangePasswordRequest;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.anyString;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest @DaoSqlTest
public class AuthControllerTest extends AbstractControllerTest { public class AuthControllerTest extends AbstractControllerTest {
@After
public void tearDown() throws Exception {
loginSysAdmin();
SecuritySettings securitySettings = doGet("/api/admin/securitySettings", SecuritySettings.class);
securitySettings.getPasswordPolicy().setMaximumLength(72);
securitySettings.getPasswordPolicy().setForceUserToResetPasswordIfNotValid(false);
doPost("/api/admin/securitySettings", securitySettings).andExpect(status().isOk());
}
@Test @Test
public void testGetUser() throws Exception { public void testGetUser() throws Exception {
@ -84,4 +106,65 @@ public class AuthControllerTest extends AbstractControllerTest {
.andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name()))) .andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name())))
.andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL))); .andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL)));
} }
@Test
public void testShouldNotUpdatePasswordWithValueLongerThanDefaultLimit() throws Exception {
loginTenantAdmin();
ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest();
changePasswordRequest.setCurrentPassword("tenant");
changePasswordRequest.setNewPassword(RandomStringUtils.randomAlphanumeric(73));
doPost("/api/auth/changePassword", changePasswordRequest)
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message", is("Password must be no more than 72 characters in length.")));
}
@Test
public void testShouldNotAuthorizeUserIfHisPasswordBecameTooLong() throws Exception {
loginTenantAdmin();
ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest();
changePasswordRequest.setCurrentPassword("tenant");
String newPassword = RandomStringUtils.randomAlphanumeric(16);
changePasswordRequest.setNewPassword(newPassword);
doPost("/api/auth/changePassword", changePasswordRequest)
.andExpect(status().isOk());
loginUser(TENANT_ADMIN_EMAIL, newPassword);
loginSysAdmin();
SecuritySettings securitySettings = doGet("/api/admin/securitySettings", SecuritySettings.class);
securitySettings.getPasswordPolicy().setMaximumLength(15);
securitySettings.getPasswordPolicy().setForceUserToResetPasswordIfNotValid(true);
doPost("/api/admin/securitySettings", securitySettings).andExpect(status().isOk());
//try to login with user password that is not valid after security settings was updated
doPost("/api/auth/login", new LoginRequest(TENANT_ADMIN_EMAIL, newPassword))
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.message", is("The entered password violates our policies. If this is your real password, please reset it.")));
}
@Test
public void testShouldNotResetPasswordToTooLongValue() throws Exception {
loginTenantAdmin();
JsonNode resetPasswordByEmailRequest = JacksonUtil.newObjectNode()
.put("email", TENANT_ADMIN_EMAIL);
doPost("/api/noauth/resetPasswordByEmail", resetPasswordByEmailRequest)
.andExpect(status().isOk());
Thread.sleep(1000);
doGet("/api/noauth/resetPassword?resetToken={resetToken}", this.currentResetPasswordToken)
.andExpect(status().isSeeOther())
.andExpect(header().string(HttpHeaders.LOCATION, "/login/resetPassword?resetToken=" + this.currentResetPasswordToken));
String newPassword = RandomStringUtils.randomAlphanumeric(73);
JsonNode resetPasswordRequest = JacksonUtil.newObjectNode()
.put("resetToken", this.currentResetPasswordToken)
.put("password", newPassword);
Mockito.doNothing().when(mailService).sendPasswordWasResetEmail(anyString(), anyString());
doPost("/api/noauth/resetPassword", resetPasswordRequest)
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message",
is("Password must be no more than 72 characters in length.")));
}
} }

65
application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java

@ -22,12 +22,7 @@ import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.AdminSettings;
@ -46,7 +41,6 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.dao.device.DeviceDao;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import java.nio.file.Files; import java.nio.file.Files;
@ -68,7 +62,6 @@ import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.WINDOWS;
@TestPropertySource(properties = { @TestPropertySource(properties = {
"device.connectivity.mqtts.pem_cert_file=/tmp/" + CA_ROOT_CERT_PEM "device.connectivity.mqtts.pem_cert_file=/tmp/" + CA_ROOT_CERT_PEM
}) })
@ContextConfiguration(classes = {DeviceConnectivityControllerTest.Config.class})
@DaoSqlTest @DaoSqlTest
public class DeviceConnectivityControllerTest extends AbstractControllerTest { public class DeviceConnectivityControllerTest extends AbstractControllerTest {
@ -102,14 +95,6 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
private DeviceProfileId mqttDeviceProfileId; private DeviceProfileId mqttDeviceProfileId;
private DeviceProfileId coapDeviceProfileId; private DeviceProfileId coapDeviceProfileId;
static class Config {
@Bean
@Primary
public DeviceDao deviceDao(DeviceDao deviceDao) {
return Mockito.mock(DeviceDao.class, AdditionalAnswers.delegatesTo(deviceDao));
}
}
@Before @Before
public void beforeTest() throws Exception { public void beforeTest() throws Exception {
loginSysAdmin(); loginSysAdmin();
@ -234,8 +219,8 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t v1/devices/me/telemetry " + assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t v1/devices/me/telemetry " +
"-u \"%s\" -m \"{temperature:25}\"", "-u \"%s\" -m \"{temperature:25}\"",
credentials.getCredentialsId())); credentials.getCredentialsId()));
assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download"); assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/mqtts/certificate/download");
assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 " + assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile " + CA_ROOT_CERT_PEM + " -h localhost -p 8883 " +
"-t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId())); "-t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId()));
JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER); JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER);
@ -243,8 +228,8 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
" -p 1883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", " -p 1883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"",
credentials.getCredentialsId())); credentials.getCredentialsId()));
assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/mosquitto-clients " + assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/mosquitto-clients " +
"/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + "/bin/sh -c \"curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/mqtts/certificate/download && " +
"mosquitto_pub -d -q 1 --cafile ca-root.pem -h host.docker.internal -p 8883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"\"", "mosquitto_pub -d -q 1 --cafile " + CA_ROOT_CERT_PEM + " -h host.docker.internal -p 8883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"\"",
credentials.getCredentialsId())); credentials.getCredentialsId()));
JsonNode linuxCoapCommands = commands.get(COAP); JsonNode linuxCoapCommands = commands.get(COAP);
@ -278,8 +263,8 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
JsonNode mqttCommands = commands.get(MQTT); JsonNode mqttCommands = commands.get(MQTT);
assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t %s " + assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t %s " +
"-u \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); "-u \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId()));
assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download"); assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/mqtts/certificate/download");
assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 " + assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile " + CA_ROOT_CERT_PEM + " -h localhost -p 8883 " +
"-t %s -u \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); "-t %s -u \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId()));
JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER); JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER);
@ -287,8 +272,8 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
" -p 1883 -t %s -u \"%s\" -m \"{temperature:25}\"", " -p 1883 -t %s -u \"%s\" -m \"{temperature:25}\"",
DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId()));
assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/mosquitto-clients " + assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/mosquitto-clients " +
"/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + "/bin/sh -c \"curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/mqtts/certificate/download && " +
"mosquitto_pub -d -q 1 --cafile ca-root.pem -h host.docker.internal -p 8883 -t %s -u \"%s\" -m \"{temperature:25}\"\"", "mosquitto_pub -d -q 1 --cafile " + CA_ROOT_CERT_PEM + " -h host.docker.internal -p 8883 -t %s -u \"%s\" -m \"{temperature:25}\"\"",
DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId()));
} }
@ -309,7 +294,7 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
doGetTyped("/api/device-connectivity/gateway-launch/" + savedDevice.getId().getId(), new TypeReference<>() { doGetTyped("/api/device-connectivity/gateway-launch/" + savedDevice.getId().getId(), new TypeReference<>() {
}); });
String expectedContainerName = deviceName.replaceAll("[^A-Za-z0-9_.-]", "");; String expectedContainerName = deviceName.replaceAll("[^A-Za-z0-9_.-]", "");
JsonNode dockerMqttCommands = commands.get(MQTT); JsonNode dockerMqttCommands = commands.get(MQTT);
assertThat(dockerMqttCommands.get(LINUX).asText()).isEqualTo(String.format("docker run -it -v ~/.tb-gateway/logs:/thingsboard_gateway/logs -v ~/.tb-gateway/extensions:/thingsboard_gateway/extensions -v ~/.tb-gateway/config:/thingsboard_gateway/config --name " + expectedContainerName + " --add-host=host.docker.internal:host-gateway -p 60000-61000:60000-61000 -e host=host.docker.internal -e port=1883 -e accessToken=%s --restart always thingsboard/tb-gateway", credentials.getCredentialsId())); assertThat(dockerMqttCommands.get(LINUX).asText()).isEqualTo(String.format("docker run -it -v ~/.tb-gateway/logs:/thingsboard_gateway/logs -v ~/.tb-gateway/extensions:/thingsboard_gateway/extensions -v ~/.tb-gateway/config:/thingsboard_gateway/config --name " + expectedContainerName + " --add-host=host.docker.internal:host-gateway -p 60000-61000:60000-61000 -e host=host.docker.internal -e port=1883 -e accessToken=%s --restart always thingsboard/tb-gateway", credentials.getCredentialsId()));
@ -349,16 +334,16 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
JsonNode mqttCommands = commands.get(MQTT); JsonNode mqttCommands = commands.get(MQTT);
assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h ::1 -p 1883 -t v1/devices/me/telemetry " + assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h ::1 -p 1883 -t v1/devices/me/telemetry " +
"-u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId())); "-u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId()));
assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download"); assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/mqtts/certificate/download");
assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile ca-root.pem -h ::1 -p 8883 " + assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile " + CA_ROOT_CERT_PEM + " -h ::1 -p 8883 " +
"-t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId())); "-t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId()));
JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER); JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER);
assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h host.docker.internal" + assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h host.docker.internal" +
" -p 1883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId())); " -p 1883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId()));
assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/mosquitto-clients " + assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/mosquitto-clients " +
"/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + "/bin/sh -c \"curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/mqtts/certificate/download && " +
"mosquitto_pub -d -q 1 --cafile ca-root.pem -h host.docker.internal -p 8883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"\"", "mosquitto_pub -d -q 1 --cafile " + CA_ROOT_CERT_PEM + " -h host.docker.internal -p 8883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"\"",
credentials.getCredentialsId())); credentials.getCredentialsId()));
JsonNode linuxCoapCommands = commands.get(COAP); JsonNode linuxCoapCommands = commands.get(COAP);
@ -403,16 +388,16 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
JsonNode mqttCommands = commands.get(MQTT); JsonNode mqttCommands = commands.get(MQTT);
assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h 1:1:1:1:1:1:1:1 -p 1883 -t v1/devices/me/telemetry " + assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h 1:1:1:1:1:1:1:1 -p 1883 -t v1/devices/me/telemetry " +
"-u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId())); "-u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId()));
assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download"); assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/mqtts/certificate/download");
assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile ca-root.pem -h 1:1:1:1:1:1:1:1 -p 8883 " + assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile " + CA_ROOT_CERT_PEM + " -h 1:1:1:1:1:1:1:1 -p 8883 " +
"-t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId())); "-t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId()));
JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER); JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER);
assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h 1:1:1:1:1:1:1:1" + assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h 1:1:1:1:1:1:1:1" +
" -p 1883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId())); " -p 1883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId()));
assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " + assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " +
"/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + "/bin/sh -c \"curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/mqtts/certificate/download && " +
"mosquitto_pub -d -q 1 --cafile ca-root.pem -h 1:1:1:1:1:1:1:1 -p 8883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"\"", "mosquitto_pub -d -q 1 --cafile " + CA_ROOT_CERT_PEM + " -h 1:1:1:1:1:1:1:1 -p 8883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"\"",
credentials.getCredentialsId())); credentials.getCredentialsId()));
JsonNode linuxCoapCommands = commands.get(COAP); JsonNode linuxCoapCommands = commands.get(COAP);
@ -459,8 +444,8 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
JsonNode mqttCommands = commands.get(MQTT); JsonNode mqttCommands = commands.get(MQTT);
assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t %s " + assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t %s " +
"-i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); "-i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password));
assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download"); assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/mqtts/certificate/download");
assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 " + assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile " + CA_ROOT_CERT_PEM + " -h localhost -p 8883 " +
"-t %s -i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); "-t %s -i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password));
JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER); JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER);
@ -468,8 +453,8 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
" -p 1883 -t %s -i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"", " -p 1883 -t %s -i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"",
DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); DEVICE_TELEMETRY_TOPIC, clientId, userName, password));
assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/mosquitto-clients " + assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/mosquitto-clients " +
"/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + "/bin/sh -c \"curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/mqtts/certificate/download && " +
"mosquitto_pub -d -q 1 --cafile ca-root.pem -h host.docker.internal -p 8883 -t %s -i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"\"", "mosquitto_pub -d -q 1 --cafile " + CA_ROOT_CERT_PEM + " -h host.docker.internal -p 8883 -t %s -i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"\"",
DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); DEVICE_TELEMETRY_TOPIC, clientId, userName, password));
} }
@ -682,8 +667,8 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h test.domain -p 1883 -t v1/devices/me/telemetry " + assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h test.domain -p 1883 -t v1/devices/me/telemetry " +
"-u \"%s\" -m \"{temperature:25}\"", "-u \"%s\" -m \"{temperature:25}\"",
credentials.getCredentialsId())); credentials.getCredentialsId()));
assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download"); assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/mqtts/certificate/download");
assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile ca-root.pem -h test.domain -p 8883 " + assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile " + CA_ROOT_CERT_PEM + " -h test.domain -p 8883 " +
"-t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId())); "-t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId()));
JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER); JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER);
@ -691,8 +676,8 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
" -p 1883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", " -p 1883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"",
credentials.getCredentialsId())); credentials.getCredentialsId()));
assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " + assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " +
"/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + "/bin/sh -c \"curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/mqtts/certificate/download && " +
"mosquitto_pub -d -q 1 --cafile ca-root.pem -h test.domain -p 8883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"\"", "mosquitto_pub -d -q 1 --cafile " + CA_ROOT_CERT_PEM + " -h test.domain -p 8883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"\"",
credentials.getCredentialsId())); credentials.getCredentialsId()));
JsonNode linuxCoapCommands = commands.get(COAP); JsonNode linuxCoapCommands = commands.get(COAP);

43
application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java

@ -16,7 +16,6 @@
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.java_websocket.client.WebSocketClient; import org.java_websocket.client.WebSocketClient;
@ -28,11 +27,11 @@ import org.thingsboard.server.common.data.query.EntityDataPageLink;
import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityFilter; import org.thingsboard.server.common.data.query.EntityFilter;
import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.service.ws.telemetry.cmd.TelemetryPluginCmdsWrapper; import org.thingsboard.server.service.ws.AuthCmd;
import org.thingsboard.server.service.ws.WsCmd;
import org.thingsboard.server.service.ws.WsCommandsWrapper;
import org.thingsboard.server.service.ws.telemetry.cmd.v1.AttributesSubscriptionCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v1.AttributesSubscriptionCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate;
@ -66,6 +65,12 @@ public class TbTestWebSocketClient extends WebSocketClient {
} }
public void authenticate(String token) {
WsCommandsWrapper cmdsWrapper = new WsCommandsWrapper();
cmdsWrapper.setAuthCmd(new AuthCmd(1, token));
send(JacksonUtil.toString(cmdsWrapper));
}
@Override @Override
public void onMessage(String s) { public void onMessage(String s) {
log.info("RECEIVED: {}", s); log.info("RECEIVED: {}", s);
@ -105,24 +110,6 @@ public class TbTestWebSocketClient extends WebSocketClient {
super.send(text); super.send(text);
} }
public void send(EntityDataCmd cmd) throws NotYetConnectedException {
TelemetryPluginCmdsWrapper wrapper = new TelemetryPluginCmdsWrapper();
wrapper.setEntityDataCmds(Collections.singletonList(cmd));
this.send(JacksonUtil.toString(wrapper));
}
public void send(EntityCountCmd cmd) throws NotYetConnectedException {
TelemetryPluginCmdsWrapper wrapper = new TelemetryPluginCmdsWrapper();
wrapper.setEntityCountCmds(Collections.singletonList(cmd));
this.send(JacksonUtil.toString(wrapper));
}
public void send(AlarmCountCmd cmd) throws NotYetConnectedException {
TelemetryPluginCmdsWrapper wrapper = new TelemetryPluginCmdsWrapper();
wrapper.setAlarmCountCmds(Collections.singletonList(cmd));
this.send(JacksonUtil.toString(wrapper));
}
public String waitForUpdate() { public String waitForUpdate() {
return waitForUpdate(false); return waitForUpdate(false);
} }
@ -240,11 +227,7 @@ public class TbTestWebSocketClient extends WebSocketClient {
cmd.setEntityId(entityId.getId().toString()); cmd.setEntityId(entityId.getId().toString());
cmd.setScope(scope); cmd.setScope(scope);
cmd.setKeys(String.join(",", keys)); cmd.setKeys(String.join(",", keys));
TelemetryPluginCmdsWrapper cmdsWrapper = new TelemetryPluginCmdsWrapper(); send(cmd);
cmdsWrapper.setAttrSubCmds(List.of(cmd));
JsonNode msg = JacksonUtil.valueToTree(cmdsWrapper);
((ObjectNode) msg.get("attrSubCmds").get(0)).remove("type");
send(msg.toString());
return JacksonUtil.toJsonNode(waitForReply()); return JacksonUtil.toJsonNode(waitForReply());
} }
@ -288,4 +271,10 @@ public class TbTestWebSocketClient extends WebSocketClient {
return sendEntityDataQuery(edq); return sendEntityDataQuery(edq);
} }
public void send(WsCmd... cmds) {
WsCommandsWrapper cmdsWrapper = new WsCommandsWrapper();
cmdsWrapper.setCmds(List.of(cmds));
send(JacksonUtil.toString(cmdsWrapper));
}
} }

7
application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java

@ -648,7 +648,7 @@ public class WebsocketApiTest extends AbstractControllerTest {
} }
@Test @Test
public void testEntityCountCmd_filterTypeSingularCompatibilityTest() { public void testEntityCountCmd_filterTypeSingularCompatibilityTest() throws Exception {
ObjectNode oldFormatDeviceTypeFilterSingular = JacksonUtil.newObjectNode(); ObjectNode oldFormatDeviceTypeFilterSingular = JacksonUtil.newObjectNode();
oldFormatDeviceTypeFilterSingular.put("type", "deviceType"); oldFormatDeviceTypeFilterSingular.put("type", "deviceType");
oldFormatDeviceTypeFilterSingular.put("deviceType", "default"); oldFormatDeviceTypeFilterSingular.put("deviceType", "default");
@ -667,9 +667,10 @@ public class WebsocketApiTest extends AbstractControllerTest {
ObjectNode wrapperNode = JacksonUtil.newObjectNode(); ObjectNode wrapperNode = JacksonUtil.newObjectNode();
wrapperNode.set("entityCountCmds", entityCountCmds); wrapperNode.set("entityCountCmds", entityCountCmds);
getWsClient().send(JacksonUtil.toString(wrapperNode)); wsClient = buildAndConnectWebSocketClient("/api/ws/plugins/telemetry?token=" + token);
wsClient.send(JacksonUtil.toString(wrapperNode));
EntityCountUpdate update = getWsClient().parseCountReply(getWsClient().waitForReply()); EntityCountUpdate update = wsClient.parseCountReply(wsClient.waitForReply());
Assert.assertEquals(1, update.getCmdId()); Assert.assertEquals(1, update.getCmdId());
Assert.assertEquals(1, update.getCount()); Assert.assertEquals(1, update.getCount());

29
application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java

@ -32,7 +32,10 @@ import javax.websocket.SendResult;
import javax.websocket.Session; import javax.websocket.Session;
import java.io.IOException; import java.io.IOException;
import java.util.Collection; import java.util.Collection;
import java.util.Deque;
import java.util.List; import java.util.List;
import java.util.Random;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@ -49,6 +52,7 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.willAnswer; import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.BDDMockito.willDoNothing; import static org.mockito.BDDMockito.willDoNothing;
import static org.mockito.BDDMockito.willReturn; import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
@ -79,7 +83,9 @@ class TbWebSocketHandlerTest {
asyncRemote = mock(RemoteEndpoint.Async.class); asyncRemote = mock(RemoteEndpoint.Async.class);
willReturn(asyncRemote).given(nativeSession).getAsyncRemote(); willReturn(asyncRemote).given(nativeSession).getAsyncRemote();
sessionRef = mock(WebSocketSessionRef.class, Mockito.RETURNS_DEEP_STUBS); //prevent NPE on logs sessionRef = mock(WebSocketSessionRef.class, Mockito.RETURNS_DEEP_STUBS); //prevent NPE on logs
sendHandler = spy(wsHandler.new SessionMetaData(session, sessionRef, maxMsgQueuePerSession)); TbWebSocketHandler.SessionMetaData sessionMd = wsHandler.new SessionMetaData(session, sessionRef);
sessionMd.setMaxMsgQueueSize(maxMsgQueuePerSession);
sendHandler = spy(sessionMd);
} }
@AfterEach @AfterEach
@ -157,4 +163,25 @@ class TbWebSocketHandlerTest {
verify(asyncRemote, times(1)).sendText(anyString(), any()); verify(asyncRemote, times(1)).sendText(anyString(), any());
} }
@Test
void sendHandler_onMsg_allProcessed() throws Exception {
Deque<String> msgs = new ConcurrentLinkedDeque<>();
doAnswer(inv -> msgs.add(inv.getArgument(1))).when(wsHandler).processMsg(any(), any());
for (int i = 0; i < 100; i++) {
String msg = String.valueOf(i);
executor.submit(() -> {
try {
Thread.sleep(new Random().nextInt(50));
sendHandler.onMsg(msg);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
assertThat(msgs).map(Integer::parseInt).doesNotHaveDuplicates().hasSize(100);
}
} }

3
application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java

@ -259,8 +259,9 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
@Override @Override
protected NotificationApiWsClient buildAndConnectWebSocketClient() throws URISyntaxException, InterruptedException { protected NotificationApiWsClient buildAndConnectWebSocketClient() throws URISyntaxException, InterruptedException {
NotificationApiWsClient wsClient = new NotificationApiWsClient(WS_URL + wsPort, token); NotificationApiWsClient wsClient = new NotificationApiWsClient(WS_URL + wsPort);
assertThat(wsClient.connectBlocking(TIMEOUT, TimeUnit.SECONDS)).isTrue(); assertThat(wsClient.connectBlocking(TIMEOUT, TimeUnit.SECONDS)).isTrue();
wsClient.authenticate(token);
return wsClient; return wsClient;
} }

26
application/src/test/java/org/thingsboard/server/service/notification/NotificationApiWsClient.java

@ -24,7 +24,6 @@ import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.controller.TbTestWebSocketClient; import org.thingsboard.server.controller.TbTestWebSocketClient;
import org.thingsboard.server.service.ws.notification.cmd.MarkAllNotificationsAsReadCmd; import org.thingsboard.server.service.ws.notification.cmd.MarkAllNotificationsAsReadCmd;
import org.thingsboard.server.service.ws.notification.cmd.MarkNotificationsAsReadCmd; import org.thingsboard.server.service.ws.notification.cmd.MarkNotificationsAsReadCmd;
import org.thingsboard.server.service.ws.notification.cmd.NotificationCmdsWrapper;
import org.thingsboard.server.service.ws.notification.cmd.NotificationsCountSubCmd; import org.thingsboard.server.service.ws.notification.cmd.NotificationsCountSubCmd;
import org.thingsboard.server.service.ws.notification.cmd.NotificationsSubCmd; import org.thingsboard.server.service.ws.notification.cmd.NotificationsSubCmd;
import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsCountUpdate; import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsCountUpdate;
@ -49,40 +48,27 @@ public class NotificationApiWsClient extends TbTestWebSocketClient {
private int unreadCount; private int unreadCount;
private List<Notification> notifications; private List<Notification> notifications;
public NotificationApiWsClient(String wsUrl, String token) throws URISyntaxException { public NotificationApiWsClient(String wsUrl) throws URISyntaxException {
super(new URI(wsUrl + "/api/ws/plugins/notifications?token=" + token)); super(new URI(wsUrl + "/api/ws"));
} }
public NotificationApiWsClient subscribeForUnreadNotifications(int limit) { public NotificationApiWsClient subscribeForUnreadNotifications(int limit) {
NotificationCmdsWrapper cmdsWrapper = new NotificationCmdsWrapper(); send(new NotificationsSubCmd(1, limit));
cmdsWrapper.setUnreadSubCmd(new NotificationsSubCmd(1, limit));
sendCmd(cmdsWrapper);
this.limit = limit; this.limit = limit;
return this; return this;
} }
public NotificationApiWsClient subscribeForUnreadNotificationsCount() { public NotificationApiWsClient subscribeForUnreadNotificationsCount() {
NotificationCmdsWrapper cmdsWrapper = new NotificationCmdsWrapper(); send(new NotificationsCountSubCmd(2));
cmdsWrapper.setUnreadCountSubCmd(new NotificationsCountSubCmd(2));
sendCmd(cmdsWrapper);
return this; return this;
} }
public void markNotificationAsRead(UUID... notifications) { public void markNotificationAsRead(UUID... notifications) {
NotificationCmdsWrapper cmdsWrapper = new NotificationCmdsWrapper(); send(new MarkNotificationsAsReadCmd(newCmdId(), Arrays.asList(notifications)));
cmdsWrapper.setMarkAsReadCmd(new MarkNotificationsAsReadCmd(newCmdId(), Arrays.asList(notifications)));
sendCmd(cmdsWrapper);
} }
public void markAllNotificationsAsRead() { public void markAllNotificationsAsRead() {
NotificationCmdsWrapper cmdsWrapper = new NotificationCmdsWrapper(); send(new MarkAllNotificationsAsReadCmd(newCmdId()));
cmdsWrapper.setMarkAllAsReadCmd(new MarkAllNotificationsAsReadCmd(newCmdId()));
sendCmd(cmdsWrapper);
}
public void sendCmd(NotificationCmdsWrapper cmdsWrapper) {
String cmd = JacksonUtil.toString(cmdsWrapper);
send(cmd);
} }
@Override @Override

8
application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java

@ -106,7 +106,7 @@ public class JwtTokenFactoryTest {
AccessJwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); AccessJwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
checkExpirationTime(accessToken, jwtSettings.getTokenExpirationTime()); checkExpirationTime(accessToken, jwtSettings.getTokenExpirationTime());
SecurityUser parsedSecurityUser = tokenFactory.parseAccessJwtToken(new RawAccessJwtToken(accessToken.getToken())); SecurityUser parsedSecurityUser = tokenFactory.parseAccessJwtToken(accessToken.getToken());
assertThat(parsedSecurityUser.getId()).isEqualTo(securityUser.getId()); assertThat(parsedSecurityUser.getId()).isEqualTo(securityUser.getId());
assertThat(parsedSecurityUser.getEmail()).isEqualTo(securityUser.getEmail()); assertThat(parsedSecurityUser.getEmail()).isEqualTo(securityUser.getEmail());
assertThat(parsedSecurityUser.getUserPrincipal()).matches(userPrincipal -> { assertThat(parsedSecurityUser.getUserPrincipal()).matches(userPrincipal -> {
@ -135,7 +135,7 @@ public class JwtTokenFactoryTest {
JwtToken refreshToken = tokenFactory.createRefreshToken(securityUser); JwtToken refreshToken = tokenFactory.createRefreshToken(securityUser);
checkExpirationTime(refreshToken, jwtSettings.getRefreshTokenExpTime()); checkExpirationTime(refreshToken, jwtSettings.getRefreshTokenExpTime());
SecurityUser parsedSecurityUser = tokenFactory.parseRefreshToken(new RawAccessJwtToken(refreshToken.getToken())); SecurityUser parsedSecurityUser = tokenFactory.parseRefreshToken(refreshToken.getToken());
assertThat(parsedSecurityUser.getId()).isEqualTo(securityUser.getId()); assertThat(parsedSecurityUser.getId()).isEqualTo(securityUser.getId());
assertThat(parsedSecurityUser.getUserPrincipal()).matches(userPrincipal -> { assertThat(parsedSecurityUser.getUserPrincipal()).matches(userPrincipal -> {
return userPrincipal.getType().equals(securityUser.getUserPrincipal().getType()) return userPrincipal.getType().equals(securityUser.getUserPrincipal().getType())
@ -159,7 +159,7 @@ public class JwtTokenFactoryTest {
JwtToken preVerificationToken = tokenFactory.createPreVerificationToken(securityUser, tokenLifetime); JwtToken preVerificationToken = tokenFactory.createPreVerificationToken(securityUser, tokenLifetime);
checkExpirationTime(preVerificationToken, tokenLifetime); checkExpirationTime(preVerificationToken, tokenLifetime);
SecurityUser parsedSecurityUser = tokenFactory.parseAccessJwtToken(new RawAccessJwtToken(preVerificationToken.getToken())); SecurityUser parsedSecurityUser = tokenFactory.parseAccessJwtToken(preVerificationToken.getToken());
assertThat(parsedSecurityUser.getId()).isEqualTo(securityUser.getId()); assertThat(parsedSecurityUser.getId()).isEqualTo(securityUser.getId());
assertThat(parsedSecurityUser.getAuthority()).isEqualTo(Authority.PRE_VERIFICATION_TOKEN); assertThat(parsedSecurityUser.getAuthority()).isEqualTo(Authority.PRE_VERIFICATION_TOKEN);
assertThat(parsedSecurityUser.getTenantId()).isEqualTo(securityUser.getTenantId()); assertThat(parsedSecurityUser.getTenantId()).isEqualTo(securityUser.getTenantId());
@ -198,7 +198,7 @@ public class JwtTokenFactoryTest {
} }
private void checkExpirationTime(JwtToken jwtToken, int tokenLifetime) { private void checkExpirationTime(JwtToken jwtToken, int tokenLifetime) {
Claims claims = tokenFactory.parseTokenClaims(jwtToken).getBody(); Claims claims = tokenFactory.parseTokenClaims(jwtToken.getToken()).getBody();
assertThat(claims.getExpiration()).matches(actualExpirationTime -> { assertThat(claims.getExpiration()).matches(actualExpirationTime -> {
Calendar expirationTime = Calendar.getInstance(); Calendar expirationTime = Calendar.getInstance();
expirationTime.setTime(new Date()); expirationTime.setTime(new Date());

4
application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java

@ -114,12 +114,12 @@ public class TokenOutdatingTest {
// Token outdatage time is rounded to 1 sec. Need to wait before outdating so that outdatage time is strictly after token issue time // Token outdatage time is rounded to 1 sec. Need to wait before outdating so that outdatage time is strictly after token issue time
SECONDS.sleep(1); SECONDS.sleep(1);
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId())); eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId()));
assertTrue(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId())); assertTrue(tokenOutdatingService.isOutdated(jwtToken.getToken(), securityUser.getId()));
SECONDS.sleep(1); SECONDS.sleep(1);
JwtToken newJwtToken = tokenFactory.createAccessJwtToken(securityUser); JwtToken newJwtToken = tokenFactory.createAccessJwtToken(securityUser);
assertFalse(tokenOutdatingService.isOutdated(newJwtToken, securityUser.getId())); assertFalse(tokenOutdatingService.isOutdated(newJwtToken.getToken(), securityUser.getId()));
} }
@Test @Test

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

@ -63,7 +63,7 @@ import org.thingsboard.server.common.data.query.SingleEntityFilter;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.ws.telemetry.cmd.TelemetryPluginCmdsWrapper; import org.thingsboard.server.service.ws.telemetry.cmd.TelemetryCmdsWrapper;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd;
@ -229,10 +229,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
EntityDataCmd cmd = new EntityDataCmd(1, edq, null, latestCmd, null); EntityDataCmd cmd = new EntityDataCmd(1, edq, null, latestCmd, null);
TelemetryPluginCmdsWrapper wrapper = new TelemetryPluginCmdsWrapper(); getWsClient().send(cmd);
wrapper.setEntityDataCmds(Collections.singletonList(cmd));
getWsClient().send(JacksonUtil.toString(wrapper));
getWsClient().waitForReply(); getWsClient().waitForReply();
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();

1
common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java

@ -38,7 +38,6 @@ public class DashboardInfo extends BaseData<DashboardId> implements HasName, Has
@NoXss @NoXss
@Length(fieldName = "title") @Length(fieldName = "title")
private String title; private String title;
@Length(fieldName = "image", max = 1000000)
private String image; private String image;
@Valid @Valid
private Set<ShortCustomerInfo> assignedCustomers; private Set<ShortCustomerInfo> assignedCustomers;

1
common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java

@ -54,7 +54,6 @@ public class DeviceProfile extends BaseData<DeviceProfileId> implements HasName,
@NoXss @NoXss
@ApiModelProperty(position = 11, value = "Device Profile description. ") @ApiModelProperty(position = 11, value = "Device Profile description. ")
private String description; private String description;
@Length(fieldName = "image", max = 1000000)
@ApiModelProperty(position = 12, value = "Either URL or Base64 data of the icon. Used in the mobile application to visualize set of device profiles in the grid view. ") @ApiModelProperty(position = 12, value = "Either URL or Base64 data of the icon. Used in the mobile application to visualize set of device profiles in the grid view. ")
private String image; private String image;
private boolean isDefault; private boolean isDefault;

1
common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java

@ -52,7 +52,6 @@ public class AssetProfile extends BaseData<AssetProfileId> implements HasName, H
@NoXss @NoXss
@ApiModelProperty(position = 11, value = "Asset Profile description. ") @ApiModelProperty(position = 11, value = "Asset Profile description. ")
private String description; private String description;
@Length(fieldName = "image", max = 1000000)
@ApiModelProperty(position = 12, value = "Either URL or Base64 data of the icon. Used in the mobile application to visualize set of asset profiles in the grid view. ") @ApiModelProperty(position = 12, value = "Either URL or Base64 data of the icon. Used in the mobile application to visualize set of asset profiles in the grid view. ")
private String image; private String image;
private boolean isDefault; private boolean isDefault;

3
common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java

@ -29,7 +29,8 @@ public enum ThingsboardErrorCode {
ITEM_NOT_FOUND(32), ITEM_NOT_FOUND(32),
TOO_MANY_REQUESTS(33), TOO_MANY_REQUESTS(33),
TOO_MANY_UPDATES(34), TOO_MANY_UPDATES(34),
SUBSCRIPTION_VIOLATION(40); SUBSCRIPTION_VIOLATION(40),
PASSWORD_VIOLATION(45);
private int errorCode; private int errorCode;

4
common/data/src/main/java/org/thingsboard/server/common/data/security/model/UserPasswordPolicy.java

@ -27,6 +27,8 @@ public class UserPasswordPolicy implements Serializable {
@ApiModelProperty(position = 1, value = "Minimum number of symbols in the password." ) @ApiModelProperty(position = 1, value = "Minimum number of symbols in the password." )
private Integer minimumLength; private Integer minimumLength;
@ApiModelProperty(position = 1, value = "Maximum number of symbols in the password." )
private Integer maximumLength;
@ApiModelProperty(position = 1, value = "Minimum number of uppercase letters in the password." ) @ApiModelProperty(position = 1, value = "Minimum number of uppercase letters in the password." )
private Integer minimumUppercaseLetters; private Integer minimumUppercaseLetters;
@ApiModelProperty(position = 1, value = "Minimum number of lowercase letters in the password." ) @ApiModelProperty(position = 1, value = "Minimum number of lowercase letters in the password." )
@ -37,6 +39,8 @@ public class UserPasswordPolicy implements Serializable {
private Integer minimumSpecialCharacters; private Integer minimumSpecialCharacters;
@ApiModelProperty(position = 1, value = "Allow whitespaces") @ApiModelProperty(position = 1, value = "Allow whitespaces")
private Boolean allowWhitespaces = true; private Boolean allowWhitespaces = true;
@ApiModelProperty(position = 1, value = "Force user to update password if existing one does not pass validation")
private Boolean forceUserToResetPasswordIfNotValid = false;
@ApiModelProperty(position = 1, value = "Password expiration period (days). Force expiration of the password." ) @ApiModelProperty(position = 1, value = "Password expiration period (days). Force expiration of the password." )
private Integer passwordExpirationPeriodDays; private Integer passwordExpirationPeriodDays;

1
common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java

@ -32,7 +32,6 @@ import org.thingsboard.server.common.data.validation.NoXss;
@JsonPropertyOrder({ "fqn", "name", "deprecated", "image", "description", "descriptor", "externalId" }) @JsonPropertyOrder({ "fqn", "name", "deprecated", "image", "description", "descriptor", "externalId" })
public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantId, HasImage, ExportableEntity<WidgetTypeId> { public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantId, HasImage, ExportableEntity<WidgetTypeId> {
@Length(fieldName = "image", max = 1000000)
@ApiModelProperty(position = 9, value = "Relative or external image URL. Replaced with image data URL (Base64) in case of relative URL and 'inlineImages' option enabled.") @ApiModelProperty(position = 9, value = "Relative or external image URL. Replaced with image data URL (Base64) in case of relative URL and 'inlineImages' option enabled.")
private String image; private String image;
@NoXss @NoXss

2
common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java

@ -57,7 +57,6 @@ public class WidgetsBundle extends BaseData<WidgetsBundleId> implements HasName,
@ApiModelProperty(position = 5, value = "Title used in search and UI", accessMode = ApiModelProperty.AccessMode.READ_ONLY) @ApiModelProperty(position = 5, value = "Title used in search and UI", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String title; private String title;
@Length(fieldName = "image", max = 1000000)
@Getter @Getter
@Setter @Setter
@ApiModelProperty(position = 6, value = "Relative or external image URL. Replaced with image data URL (Base64) in case of relative URL and 'inlineImages' option enabled.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) @ApiModelProperty(position = 6, value = "Relative or external image URL. Replaced with image data URL (Base64) in case of relative URL and 'inlineImages' option enabled.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
@ -66,7 +65,6 @@ public class WidgetsBundle extends BaseData<WidgetsBundleId> implements HasName,
@NoXss @NoXss
@Length(fieldName = "description", max = 1024) @Length(fieldName = "description", max = 1024)
@Getter @Getter
@Setter @Setter
@ApiModelProperty(position = 7, value = "Description", accessMode = ApiModelProperty.AccessMode.READ_ONLY) @ApiModelProperty(position = 7, value = "Description", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String description; private String description;

9
dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java

@ -27,7 +27,6 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Base64Utils; import org.springframework.util.Base64Utils;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
@ -274,7 +273,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic
return resourceInfoDao.findSystemOrTenantImageByEtag(tenantId, ResourceType.IMAGE, etag); return resourceInfoDao.findSystemOrTenantImageByEtag(tenantId, ResourceType.IMAGE, etag);
} }
@Transactional(propagation = Propagation.NOT_SUPPORTED)// we don't want transaction to rollback in case of an image processing failure @Transactional(noRollbackFor = Exception.class) // we don't want transaction to rollback in case of an image processing failure
@Override @Override
public boolean replaceBase64WithImageUrl(HasImage entity, String type) { public boolean replaceBase64WithImageUrl(HasImage entity, String type) {
log.trace("Executing replaceBase64WithImageUrl [{}] [{}] [{}]", entity.getTenantId(), type, entity.getName()); log.trace("Executing replaceBase64WithImageUrl [{}] [{}] [{}]", entity.getTenantId(), type, entity.getName());
@ -289,7 +288,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic
return result.isUpdated(); return result.isUpdated();
} }
@Transactional(propagation = Propagation.NOT_SUPPORTED)// we don't want transaction to rollback in case of an image processing failure @Transactional(noRollbackFor = Exception.class) // we don't want transaction to rollback in case of an image processing failure
@Override @Override
public boolean replaceBase64WithImageUrl(WidgetTypeDetails entity) { public boolean replaceBase64WithImageUrl(WidgetTypeDetails entity) {
log.trace("Executing replaceBase64WithImageUrl [{}] [WidgetTypeDetails] [{}]", entity.getTenantId(), entity.getId()); log.trace("Executing replaceBase64WithImageUrl [{}] [WidgetTypeDetails] [{}]", entity.getTenantId(), entity.getId());
@ -315,7 +314,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic
return updated; return updated;
} }
@Transactional(propagation = Propagation.NOT_SUPPORTED)// we don't want transaction to rollback in case of an image processing failure @Transactional(noRollbackFor = Exception.class) // we don't want transaction to rollback in case of an image processing failure
@Override @Override
public boolean replaceBase64WithImageUrl(Dashboard entity) { public boolean replaceBase64WithImageUrl(Dashboard entity) {
log.trace("Executing replaceBase64WithImageUrl [{}] [Dashboard] [{}]", entity.getTenantId(), entity.getId()); log.trace("Executing replaceBase64WithImageUrl [{}] [Dashboard] [{}]", entity.getTenantId(), entity.getId());
@ -463,6 +462,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic
} }
return UpdateResult.of(false, data); return UpdateResult.of(false, data);
} }
} else {
log.debug("[{}] Using existing image {} ({} - '{}') for '{}'", tenantId, imageInfo.getResourceKey(), imageInfo.getTenantId(), imageInfo.getName(), name);
} }
return UpdateResult.of(true, DataConstants.TB_IMAGE_PREFIX + imageInfo.getLink()); return UpdateResult.of(true, DataConstants.TB_IMAGE_PREFIX + imageInfo.getLink());
} }

3
dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.service;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.BaseData;
@ -129,7 +130,7 @@ public abstract class DataValidator<D extends BaseData<?>> {
EntityType entityType) { EntityType entityType) {
if (maxSumDataSize > 0) { if (maxSumDataSize > 0) {
if (dataDao.sumDataSizeByTenantId(tenantId) + currentDataSize > maxSumDataSize) { if (dataDao.sumDataSizeByTenantId(tenantId) + currentDataSize > maxSumDataSize) {
throw new DataValidationException(String.format("%ss total size exceeds the maximum of " + maxSumDataSize + " bytes", entityType.getNormalName())); throw new DataValidationException(String.format("%ss total size exceeds the maximum of " + FileUtils.byteCountToDisplaySize(maxSumDataSize), entityType.getNormalName()));
} }
} }
} }

33
dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java

@ -15,12 +15,14 @@
*/ */
package org.thingsboard.server.dao.service.validator; package org.thingsboard.server.dao.service.validator;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.widget.BaseWidgetType; import org.thingsboard.server.common.data.widget.BaseWidgetType;
@ -83,19 +85,8 @@ public class ResourceDataValidator extends DataValidator<TbResource> {
if (resource.getResourceType() == null) { if (resource.getResourceType() == null) {
throw new DataValidationException("Resource type should be specified!"); throw new DataValidationException("Resource type should be specified!");
} }
if (!resource.getTenantId().isSysTenantId() && resource.getData() != null) { if (resource.getData() != null) {
DefaultTenantProfileConfiguration profileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration(); validateResourceSize(resource.getTenantId(), resource.getId(), resource.getData().length);
long maxResourceSize = profileConfiguration.getMaxResourceSize();
if (maxResourceSize > 0 && resource.getData().length > maxResourceSize) {
throw new IllegalArgumentException("Resource exceeds the maximum size of " + maxResourceSize + " bytes");
}
long maxSumResourcesDataInBytes = profileConfiguration.getMaxResourcesInBytes();
int dataSize = resource.getData().length;
if (resource.getId() != null) {
long prevSize = resourceDao.getResourceSize(tenantId, resource.getId());
dataSize -= prevSize;
}
validateMaxSumDataSizePerTenant(tenantId, resourceDao, maxSumResourcesDataInBytes, dataSize, TB_RESOURCE);
} }
if (StringUtils.isEmpty(resource.getFileName())) { if (StringUtils.isEmpty(resource.getFileName())) {
throw new DataValidationException("Resource file name should be specified!"); throw new DataValidationException("Resource file name should be specified!");
@ -108,6 +99,22 @@ public class ResourceDataValidator extends DataValidator<TbResource> {
} }
} }
public void validateResourceSize(TenantId tenantId, TbResourceId resourceId, long dataSize) {
if (!tenantId.isSysTenantId()) {
DefaultTenantProfileConfiguration profileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration();
long maxResourceSize = profileConfiguration.getMaxResourceSize();
if (maxResourceSize > 0 && dataSize > maxResourceSize) {
throw new IllegalArgumentException("Resource exceeds the maximum size of " + FileUtils.byteCountToDisplaySize(maxResourceSize));
}
long maxSumResourcesDataInBytes = profileConfiguration.getMaxResourcesInBytes();
if (resourceId != null) {
long prevSize = resourceDao.getResourceSize(tenantId, resourceId);
dataSize -= prevSize;
}
validateMaxSumDataSizePerTenant(tenantId, resourceDao, maxSumResourcesDataInBytes, dataSize, TB_RESOURCE);
}
}
@Override @Override
public void validateDelete(TenantId tenantId, EntityId resourceId) { public void validateDelete(TenantId tenantId, EntityId resourceId) {
List<WidgetTypeDetails> widgets = widgetTypeDao.findWidgetTypesInfosByTenantIdAndResourceId(tenantId.getId(), List<WidgetTypeDetails> widgets = widgetTypeDao.findWidgetTypesInfosByTenantIdAndResourceId(tenantId.getId(),

15
dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java

@ -84,16 +84,17 @@ public class TbSqlBlockingQueue<E> implements TbSqlQueue<E> {
} }
} }
} catch (Throwable t) { } catch (Throwable t) {
log.error("[{}] Failed to save {} entities", logName, entities.size(), t);
try {
stats.incrementFailed(entities.size());
entities.forEach(entityFutureWrapper -> entityFutureWrapper.getFuture().setException(t));
} catch (Throwable th) {
log.error("[{}] Failed to set future exception", logName, th);
}
if (t instanceof InterruptedException) { if (t instanceof InterruptedException) {
log.info("[{}] Queue polling was interrupted", logName); log.info("[{}] Queue polling was interrupted", logName);
break; break;
} else {
log.error("[{}] Failed to save {} entities", logName, entities.size(), t);
try {
stats.incrementFailed(entities.size());
entities.forEach(entityFutureWrapper -> entityFutureWrapper.getFuture().setException(t));
} catch (Throwable th) {
log.error("[{}] Failed to set future exception", logName, th);
}
} }
} finally { } finally {
entities.clear(); entities.clear();

2
dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java

@ -66,7 +66,7 @@ public interface TbResourceInfoRepository extends JpaRepository<TbResourceInfoEn
List<TbResourceInfoEntity> findByTenantIdAndEtagAndResourceKeyStartingWith(UUID tenantId, String etag, String query); List<TbResourceInfoEntity> findByTenantIdAndEtagAndResourceKeyStartingWith(UUID tenantId, String etag, String query);
@Query(value = "SELECT * FROM resource r WHERE (r.tenant_id = '13814000-1dd2-11b2-8080-808080808080' OR r.tenant_id = :tenantId) " + @Query(value = "SELECT * FROM resource r WHERE (r.tenant_id = '13814000-1dd2-11b2-8080-808080808080' OR r.tenant_id = :tenantId) " +
"AND r.resource_type = :resourceType AND r.etag = :etag LIMIT 1", nativeQuery = true) "AND r.resource_type = :resourceType AND r.etag = :etag ORDER BY created_time, id LIMIT 1", nativeQuery = true)
TbResourceInfoEntity findSystemOrTenantImageByEtag(@Param("tenantId") UUID tenantId, TbResourceInfoEntity findSystemOrTenantImageByEtag(@Param("tenantId") UUID tenantId,
@Param("resourceType") String resourceType, @Param("resourceType") String resourceType,
@Param("etag") String etag); @Param("etag") String etag);

32
dao/src/test/java/org/thingsboard/server/dao/util/DeviceConnectivityUtilTest.java

@ -0,0 +1,32 @@
/**
* Copyright © 2016-2023 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.util;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class DeviceConnectivityUtilTest {
@Test
void testRootCaPemNaming() {
assertThat(DeviceConnectivityUtil.CA_ROOT_CERT_PEM).contains("root");
assertThat(DeviceConnectivityUtil.CA_ROOT_CERT_PEM).contains("ca");
assertThat(DeviceConnectivityUtil.CA_ROOT_CERT_PEM).endsWith(".pem");
assertThat(DeviceConnectivityUtil.CA_ROOT_CERT_PEM).doesNotContainAnyWhitespaces();
}
}

3
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java

@ -57,6 +57,7 @@ import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeEventService;
import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.nosql.CassandraStatementTask; import org.thingsboard.server.dao.nosql.CassandraStatementTask;
import org.thingsboard.server.dao.nosql.TbResultSetFuture; import org.thingsboard.server.dao.nosql.TbResultSetFuture;
@ -393,4 +394,6 @@ public interface TbContext {
WidgetTypeService getWidgetTypeService(); WidgetTypeService getWidgetTypeService();
RuleEngineApiUsageStateService getRuleEngineApiUsageStateService(); RuleEngineApiUsageStateService getRuleEngineApiUsageStateService();
EntityService getEntityService();
} }

2
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js

File diff suppressed because one or more lines are too long

108
ui-ngx/src/app/core/ws/notification-websocket.service.ts

@ -15,117 +15,45 @@
/// ///
import { Inject, Injectable, NgZone } from '@angular/core'; import { Inject, Injectable, NgZone } from '@angular/core';
import {
TelemetryPluginCmdsWrapper,
TelemetrySubscriber,
WebsocketDataMsg
} from '@shared/models/telemetry/telemetry.models';
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { AuthService } from '@core/auth/auth.service'; import { AuthService } from '@core/auth/auth.service';
import { WINDOW } from '@core/services/window.service'; import { WINDOW } from '@core/services/window.service';
import {
isNotificationCountUpdateMsg,
isNotificationsUpdateMsg,
MarkAllAsReadCmd,
MarkAsReadCmd,
NotificationCountUpdate,
NotificationPluginCmdWrapper,
NotificationSubscriber,
NotificationsUpdate,
UnreadCountSubCmd,
UnreadSubCmd,
UnsubscribeCmd,
WebsocketNotificationMsg
} from '@shared/models/websocket/notification-ws.models';
import { WebsocketService } from '@core/ws/websocket.service'; import { WebsocketService } from '@core/ws/websocket.service';
// @dynamic // @dynamic
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class NotificationWebsocketService extends WebsocketService<NotificationSubscriber> { export class NotificationWebsocketService extends WebsocketService<TelemetrySubscriber> {
cmdWrapper: NotificationPluginCmdWrapper; constructor(private telemetryWebsocketService: TelemetryWebsocketService,
protected store: Store<AppState>,
constructor(protected store: Store<AppState>,
protected authService: AuthService, protected authService: AuthService,
protected ngZone: NgZone, protected ngZone: NgZone,
@Inject(WINDOW) protected window: Window) { @Inject(WINDOW) protected window: Window) {
super(store, authService, ngZone, 'api/ws/plugins/notifications', new NotificationPluginCmdWrapper(), window); super(store, authService, ngZone, 'api/ws/plugins/telemetry', new TelemetryPluginCmdsWrapper(), window);
this.errorName = 'WebSocket Notification Error';
} }
public subscribe(subscriber: NotificationSubscriber) { public subscribe(subscriber: TelemetrySubscriber) {
this.isActive = true; this.telemetryWebsocketService.subscribe(subscriber);
subscriber.subscriptionCommands.forEach(
(subscriptionCommand) => {
const cmdId = this.nextCmdId();
this.subscribersMap.set(cmdId, subscriber);
subscriptionCommand.cmdId = cmdId;
if (subscriptionCommand instanceof UnreadCountSubCmd) {
this.cmdWrapper.unreadCountSubCmd = subscriptionCommand;
} else if (subscriptionCommand instanceof UnreadSubCmd) {
this.cmdWrapper.unreadSubCmd = subscriptionCommand;
} else if (subscriptionCommand instanceof MarkAsReadCmd) {
this.cmdWrapper.markAsReadCmd = subscriptionCommand;
this.subscribersMap.delete(cmdId);
} else if (subscriptionCommand instanceof MarkAllAsReadCmd) {
this.cmdWrapper.markAllAsReadCmd = subscriptionCommand;
this.subscribersMap.delete(cmdId);
}
}
);
if (this.cmdWrapper.unreadCountSubCmd || this.cmdWrapper.unreadSubCmd) {
this.subscribersCount++;
}
this.publishCommands();
} }
public update(subscriber: NotificationSubscriber) { public update(subscriber: TelemetrySubscriber) {
if (!this.isReconnect) { this.telemetryWebsocketService.update(subscriber);
subscriber.subscriptionCommands.forEach(
(subscriptionCommand) => {
if (subscriptionCommand.cmdId && subscriptionCommand instanceof UnreadSubCmd) {
this.cmdWrapper.unreadSubCmd = subscriptionCommand;
}
}
);
this.publishCommands();
}
} }
public unsubscribe(subscriber: NotificationSubscriber) { public unsubscribe(subscriber: TelemetrySubscriber) {
if (this.isActive) { this.telemetryWebsocketService.unsubscribe(subscriber);
subscriber.subscriptionCommands.forEach(
(subscriptionCommand) => {
if (subscriptionCommand instanceof UnreadCountSubCmd
|| subscriptionCommand instanceof UnreadSubCmd) {
const unreadCountUnsubscribeCmd = new UnsubscribeCmd();
unreadCountUnsubscribeCmd.cmdId = subscriptionCommand.cmdId;
this.cmdWrapper.unsubCmd = unreadCountUnsubscribeCmd;
}
const cmdId = subscriptionCommand.cmdId;
if (cmdId) {
this.subscribersMap.delete(cmdId);
}
}
);
this.reconnectSubscribers.delete(subscriber);
this.subscribersCount--;
this.publishCommands();
}
} }
processOnMessage(message: WebsocketNotificationMsg) { processOnMessage(message: WebsocketDataMsg) {
let subscriber: NotificationSubscriber; this.telemetryWebsocketService.processOnMessage(message);
if (isNotificationCountUpdateMsg(message)) {
subscriber = this.subscribersMap.get(message.cmdId);
if (subscriber) {
subscriber.onNotificationCountUpdate(new NotificationCountUpdate(message));
}
} else if (isNotificationsUpdateMsg(message)) {
subscriber = this.subscribersMap.get(message.cmdId);
if (subscriber) {
subscriber.onNotificationsUpdate(new NotificationsUpdate(message));
}
}
} }
} }

105
ui-ngx/src/app/core/ws/telemetry-websocket.service.ts

@ -16,28 +16,36 @@
import { Inject, Injectable, NgZone } from '@angular/core'; import { Inject, Injectable, NgZone } from '@angular/core';
import { import {
AlarmCountCmd, AlarmCountUnsubscribeCmd, AlarmCountCmd,
AlarmCountUnsubscribeCmd,
AlarmCountUpdate, AlarmCountUpdate,
AlarmDataCmd, AlarmDataCmd,
AlarmDataUnsubscribeCmd, AlarmDataUnsubscribeCmd,
AlarmDataUpdate, AlarmDataUpdate,
AttributesSubscriptionCmd,
EntityCountCmd, EntityCountCmd,
EntityCountUnsubscribeCmd, EntityCountUnsubscribeCmd,
EntityCountUpdate, EntityCountUpdate,
EntityDataCmd, EntityDataCmd,
EntityDataUnsubscribeCmd, EntityDataUnsubscribeCmd,
EntityDataUpdate, EntityDataUpdate,
GetHistoryCmd, isAlarmCountUpdateMsg, isAlarmCountUpdateMsg,
isAlarmDataUpdateMsg, isAlarmDataUpdateMsg,
isEntityCountUpdateMsg, isEntityCountUpdateMsg,
isEntityDataUpdateMsg, isEntityDataUpdateMsg,
isNotificationCountUpdateMsg,
isNotificationsUpdateMsg,
MarkAllAsReadCmd,
MarkAsReadCmd,
NotificationCountUpdate,
NotificationSubscriber,
NotificationsUpdate,
SubscriptionCmd, SubscriptionCmd,
SubscriptionUpdate, SubscriptionUpdate,
TelemetryFeature,
TelemetryPluginCmdsWrapper, TelemetryPluginCmdsWrapper,
TelemetrySubscriber, TelemetrySubscriber,
TimeseriesSubscriptionCmd, UnreadCountSubCmd,
UnreadSubCmd,
UnsubscribeCmd,
WebsocketDataMsg WebsocketDataMsg
} from '@app/shared/models/telemetry/telemetry.models'; } from '@app/shared/models/telemetry/telemetry.models';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@ -58,7 +66,7 @@ export class TelemetryWebsocketService extends WebsocketService<TelemetrySubscri
protected authService: AuthService, protected authService: AuthService,
protected ngZone: NgZone, protected ngZone: NgZone,
@Inject(WINDOW) protected window: Window) { @Inject(WINDOW) protected window: Window) {
super(store, authService, ngZone, 'api/ws/plugins/telemetry', new TelemetryPluginCmdsWrapper(), window); super(store, authService, ngZone, 'api/ws', new TelemetryPluginCmdsWrapper(), window);
} }
public subscribe(subscriber: TelemetrySubscriber) { public subscribe(subscriber: TelemetrySubscriber) {
@ -66,25 +74,11 @@ export class TelemetryWebsocketService extends WebsocketService<TelemetrySubscri
subscriber.subscriptionCommands.forEach( subscriber.subscriptionCommands.forEach(
(subscriptionCommand) => { (subscriptionCommand) => {
const cmdId = this.nextCmdId(); const cmdId = this.nextCmdId();
this.subscribersMap.set(cmdId, subscriber); if (!(subscriptionCommand instanceof MarkAsReadCmd) && !(subscriptionCommand instanceof MarkAllAsReadCmd)) {
subscriptionCommand.cmdId = cmdId; this.subscribersMap.set(cmdId, subscriber);
if (subscriptionCommand instanceof SubscriptionCmd) {
if (subscriptionCommand.getType() === TelemetryFeature.TIMESERIES) {
this.cmdWrapper.tsSubCmds.push(subscriptionCommand as TimeseriesSubscriptionCmd);
} else {
this.cmdWrapper.attrSubCmds.push(subscriptionCommand as AttributesSubscriptionCmd);
}
} else if (subscriptionCommand instanceof GetHistoryCmd) {
this.cmdWrapper.historyCmds.push(subscriptionCommand);
} else if (subscriptionCommand instanceof EntityDataCmd) {
this.cmdWrapper.entityDataCmds.push(subscriptionCommand);
} else if (subscriptionCommand instanceof AlarmDataCmd) {
this.cmdWrapper.alarmDataCmds.push(subscriptionCommand);
} else if (subscriptionCommand instanceof EntityCountCmd) {
this.cmdWrapper.entityCountCmds.push(subscriptionCommand);
} else if (subscriptionCommand instanceof AlarmCountCmd) {
this.cmdWrapper.alarmCountCmds.push(subscriptionCommand);
} }
subscriptionCommand.cmdId = cmdId;
this.cmdWrapper.cmds.push(subscriptionCommand);
} }
); );
this.subscribersCount++; this.subscribersCount++;
@ -95,8 +89,8 @@ export class TelemetryWebsocketService extends WebsocketService<TelemetrySubscri
if (!this.isReconnect) { if (!this.isReconnect) {
subscriber.subscriptionCommands.forEach( subscriber.subscriptionCommands.forEach(
(subscriptionCommand) => { (subscriptionCommand) => {
if (subscriptionCommand.cmdId && subscriptionCommand instanceof EntityDataCmd) { if (subscriptionCommand.cmdId && (subscriptionCommand instanceof EntityDataCmd || subscriptionCommand instanceof UnreadSubCmd)) {
this.cmdWrapper.entityDataCmds.push(subscriptionCommand); this.cmdWrapper.cmds.push(subscriptionCommand);
} }
} }
); );
@ -110,27 +104,27 @@ export class TelemetryWebsocketService extends WebsocketService<TelemetrySubscri
(subscriptionCommand) => { (subscriptionCommand) => {
if (subscriptionCommand instanceof SubscriptionCmd) { if (subscriptionCommand instanceof SubscriptionCmd) {
subscriptionCommand.unsubscribe = true; subscriptionCommand.unsubscribe = true;
if (subscriptionCommand.getType() === TelemetryFeature.TIMESERIES) { this.cmdWrapper.cmds.push(subscriptionCommand);
this.cmdWrapper.tsSubCmds.push(subscriptionCommand as TimeseriesSubscriptionCmd);
} else {
this.cmdWrapper.attrSubCmds.push(subscriptionCommand as AttributesSubscriptionCmd);
}
} else if (subscriptionCommand instanceof EntityDataCmd) { } else if (subscriptionCommand instanceof EntityDataCmd) {
const entityDataUnsubscribeCmd = new EntityDataUnsubscribeCmd(); const entityDataUnsubscribeCmd = new EntityDataUnsubscribeCmd();
entityDataUnsubscribeCmd.cmdId = subscriptionCommand.cmdId; entityDataUnsubscribeCmd.cmdId = subscriptionCommand.cmdId;
this.cmdWrapper.entityDataUnsubscribeCmds.push(entityDataUnsubscribeCmd); this.cmdWrapper.cmds.push(entityDataUnsubscribeCmd);
} else if (subscriptionCommand instanceof AlarmDataCmd) { } else if (subscriptionCommand instanceof AlarmDataCmd) {
const alarmDataUnsubscribeCmd = new AlarmDataUnsubscribeCmd(); const alarmDataUnsubscribeCmd = new AlarmDataUnsubscribeCmd();
alarmDataUnsubscribeCmd.cmdId = subscriptionCommand.cmdId; alarmDataUnsubscribeCmd.cmdId = subscriptionCommand.cmdId;
this.cmdWrapper.alarmDataUnsubscribeCmds.push(alarmDataUnsubscribeCmd); this.cmdWrapper.cmds.push(alarmDataUnsubscribeCmd);
} else if (subscriptionCommand instanceof EntityCountCmd) { } else if (subscriptionCommand instanceof EntityCountCmd) {
const entityCountUnsubscribeCmd = new EntityCountUnsubscribeCmd(); const entityCountUnsubscribeCmd = new EntityCountUnsubscribeCmd();
entityCountUnsubscribeCmd.cmdId = subscriptionCommand.cmdId; entityCountUnsubscribeCmd.cmdId = subscriptionCommand.cmdId;
this.cmdWrapper.entityCountUnsubscribeCmds.push(entityCountUnsubscribeCmd); this.cmdWrapper.cmds.push(entityCountUnsubscribeCmd);
} else if (subscriptionCommand instanceof AlarmCountCmd) { } else if (subscriptionCommand instanceof AlarmCountCmd) {
const alarmCountUnsubscribeCmd = new AlarmCountUnsubscribeCmd(); const alarmCountUnsubscribeCmd = new AlarmCountUnsubscribeCmd();
alarmCountUnsubscribeCmd.cmdId = subscriptionCommand.cmdId; alarmCountUnsubscribeCmd.cmdId = subscriptionCommand.cmdId;
this.cmdWrapper.alarmCountUnsubscribeCmds.push(alarmCountUnsubscribeCmd); this.cmdWrapper.cmds.push(alarmCountUnsubscribeCmd);
} else if (subscriptionCommand instanceof UnreadCountSubCmd || subscriptionCommand instanceof UnreadSubCmd) {
const notificationsUnsubCmds = new UnsubscribeCmd();
notificationsUnsubCmds.cmdId = subscriptionCommand.cmdId;
this.cmdWrapper.cmds.push(notificationsUnsubCmds);
} }
const cmdId = subscriptionCommand.cmdId; const cmdId = subscriptionCommand.cmdId;
if (cmdId) { if (cmdId) {
@ -145,29 +139,28 @@ export class TelemetryWebsocketService extends WebsocketService<TelemetrySubscri
} }
processOnMessage(message: WebsocketDataMsg) { processOnMessage(message: WebsocketDataMsg) {
let subscriber: TelemetrySubscriber; let subscriber: TelemetrySubscriber | NotificationSubscriber;
if (isEntityDataUpdateMsg(message)) { if ('cmdId' in message && message.cmdId) {
subscriber = this.subscribersMap.get(message.cmdId);
if (subscriber) {
subscriber.onEntityData(new EntityDataUpdate(message));
}
} else if (isAlarmDataUpdateMsg(message)) {
subscriber = this.subscribersMap.get(message.cmdId); subscriber = this.subscribersMap.get(message.cmdId);
if (subscriber) { if (subscriber instanceof NotificationSubscriber) {
subscriber.onAlarmData(new AlarmDataUpdate(message)); if (isNotificationCountUpdateMsg(message)) {
} subscriber.onNotificationCountUpdate(new NotificationCountUpdate(message));
} else if (isEntityCountUpdateMsg(message)) { } else if (isNotificationsUpdateMsg(message)) {
subscriber = this.subscribersMap.get(message.cmdId); subscriber.onNotificationsUpdate(new NotificationsUpdate(message));
if (subscriber) { }
subscriber.onEntityCount(new EntityCountUpdate(message)); } else if (subscriber instanceof TelemetrySubscriber) {
} if (isEntityDataUpdateMsg(message)) {
} else if (isAlarmCountUpdateMsg(message)) { subscriber.onEntityData(new EntityDataUpdate(message));
subscriber = this.subscribersMap.get(message.cmdId); } else if (isAlarmDataUpdateMsg(message)) {
if (subscriber) { subscriber.onAlarmData(new AlarmDataUpdate(message));
subscriber.onAlarmCount(new AlarmCountUpdate(message)); } else if (isEntityCountUpdateMsg(message)) {
subscriber.onEntityCount(new EntityCountUpdate(message));
} else if (isAlarmCountUpdateMsg(message)) {
subscriber.onAlarmCount(new AlarmCountUpdate(message));
}
} }
} else if (message.subscriptionId) { } else if ('subscriptionId' in message && message.subscriptionId) {
subscriber = this.subscribersMap.get(message.subscriptionId); subscriber = this.subscribersMap.get(message.subscriptionId) as TelemetrySubscriber;
if (subscriber) { if (subscriber) {
subscriber.onData(new SubscriptionUpdate(message)); subscriber.onData(new SubscriptionUpdate(message));
} }

45
ui-ngx/src/app/core/ws/websocket.service.ts

@ -21,8 +21,13 @@ import { AuthService } from '@core/auth/auth.service';
import { NgZone } from '@angular/core'; import { NgZone } from '@angular/core';
import { selectIsAuthenticated } from '@core/auth/auth.selectors'; import { selectIsAuthenticated } from '@core/auth/auth.selectors';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
import { WebsocketNotificationMsg } from '@shared/models/websocket/notification-ws.models'; import {
import { CmdUpdateMsg } from '@shared/models/telemetry/telemetry.models'; AuthWsCmd,
CmdUpdateMsg,
NotificationSubscriber,
TelemetrySubscriber,
WebsocketDataMsg
} from '@shared/models/telemetry/telemetry.models';
import { ActionNotificationShow } from '@core/notification/notification.actions'; import { ActionNotificationShow } from '@core/notification/notification.actions';
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
@ -42,13 +47,13 @@ export abstract class WebsocketService<T extends WsSubscriber> implements WsServ
lastCmdId = 0; lastCmdId = 0;
subscribersCount = 0; subscribersCount = 0;
subscribersMap = new Map<number, T>(); subscribersMap = new Map<number, TelemetrySubscriber | NotificationSubscriber>();
reconnectSubscribers = new Set<T>(); reconnectSubscribers = new Set<WsSubscriber>();
notificationUri: string; wsUri: string;
dataStream: WebSocketSubject<CmdWrapper | CmdUpdateMsg>; dataStream: WebSocketSubject<CmdWrapper | CmdUpdateMsg | AuthWsCmd>;
errorName = 'WebSocket Error'; errorName = 'WebSocket Error';
@ -69,23 +74,23 @@ export abstract class WebsocketService<T extends WsSubscriber> implements WsServ
if (!port) { if (!port) {
port = '443'; port = '443';
} }
this.notificationUri = 'wss:'; this.wsUri = 'wss:';
} else { } else {
if (!port) { if (!port) {
port = '80'; port = '80';
} }
this.notificationUri = 'ws:'; this.wsUri = 'ws:';
} }
this.notificationUri += `//${this.window.location.hostname}:${port}/${apiEndpoint}`; this.wsUri += `//${this.window.location.hostname}:${port}/${apiEndpoint}`;
} }
abstract subscribe(subscriber: T); abstract subscribe(subscriber: WsSubscriber);
abstract update(subscriber: T); abstract update(subscriber: T);
abstract unsubscribe(subscriber: T); abstract unsubscribe(subscriber: T);
abstract processOnMessage(message: any); abstract processOnMessage(message: WebsocketDataMsg);
protected nextCmdId(): number { protected nextCmdId(): number {
this.lastCmdId++; this.lastCmdId++;
@ -158,13 +163,13 @@ export abstract class WebsocketService<T extends WsSubscriber> implements WsServ
} }
private openSocket(token: string) { private openSocket(token: string) {
const uri = `${this.notificationUri}?token=${token}`; const uri = `${this.wsUri}`;
this.dataStream = webSocket( this.dataStream = webSocket<CmdUpdateMsg>(
{ {
url: uri, url: uri,
openObserver: { openObserver: {
next: () => { next: () => {
this.onOpen(); this.onOpen(token);
} }
}, },
closeObserver: { closeObserver: {
@ -176,9 +181,9 @@ export abstract class WebsocketService<T extends WsSubscriber> implements WsServ
); );
this.dataStream.subscribe({ this.dataStream.subscribe({
next: (message) => { next: (message: CmdUpdateMsg) => {
this.ngZone.runOutsideAngular(() => { this.ngZone.runOutsideAngular(() => {
this.onMessage(message as WebsocketNotificationMsg); this.onMessage(message);
}); });
}, },
error: (error) => { error: (error) => {
@ -187,9 +192,10 @@ export abstract class WebsocketService<T extends WsSubscriber> implements WsServ
}); });
} }
private onOpen() { private onOpen(token: string) {
this.isOpening = false; this.isOpening = false;
this.isOpened = true; this.isOpened = true;
this.cmdWrapper.setAuth(token);
if (this.reconnectTimer) { if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer); clearTimeout(this.reconnectTimer);
this.reconnectTimer = null; this.reconnectTimer = null;
@ -208,11 +214,11 @@ export abstract class WebsocketService<T extends WsSubscriber> implements WsServ
} }
} }
private onMessage(message: WebsocketNotificationMsg) { private onMessage(message: CmdUpdateMsg) {
if (message.errorCode) { if (message.errorCode) {
this.showWsError(message.errorCode, message.errorMsg); this.showWsError(message.errorCode, message.errorMsg);
} else { } else {
this.processOnMessage(message); this.processOnMessage(message as WebsocketDataMsg);
} }
this.checkToClose(); this.checkToClose();
} }
@ -259,5 +265,4 @@ export abstract class WebsocketService<T extends WsSubscriber> implements WsServ
message, type: 'error' message, type: 'error'
})); }));
} }
} }

11
ui-ngx/src/app/modules/home/components/notification/notification-bell.component.ts

@ -25,11 +25,11 @@ import {
} from '@angular/core'; } from '@angular/core';
import { NotificationWebsocketService } from '@core/ws/notification-websocket.service'; import { NotificationWebsocketService } from '@core/ws/notification-websocket.service';
import { BehaviorSubject, ReplaySubject, Subscription } from 'rxjs'; import { BehaviorSubject, ReplaySubject, Subscription } from 'rxjs';
import { distinctUntilChanged, map, share, tap } from 'rxjs/operators'; import { distinctUntilChanged, map, share, skip, tap } from 'rxjs/operators';
import { MatButton } from '@angular/material/button'; import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { ShowNotificationPopoverComponent } from '@home/components/notification/show-notification-popover.component'; import { ShowNotificationPopoverComponent } from '@home/components/notification/show-notification-popover.component';
import { NotificationSubscriber } from '@shared/models/websocket/notification-ws.models'; import { NotificationSubscriber } from '@shared/models/telemetry/telemetry.models';
import { select, Store } from '@ngrx/store'; import { select, Store } from '@ngrx/store';
import { selectIsAuthenticated } from '@core/auth/auth.selectors'; import { selectIsAuthenticated } from '@core/auth/auth.selectors';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -77,11 +77,11 @@ export class NotificationBellComponent implements OnDestroy {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
this.unsubscribeSubscription();
const trigger = createVersionButton._elementRef.nativeElement; const trigger = createVersionButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) { if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger); this.popoverService.hidePopover(trigger);
} else { } else {
this.unsubscribeSubscription();
const showNotificationPopover = this.popoverService.displayPopover(trigger, this.renderer, const showNotificationPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, ShowNotificationPopoverComponent, 'bottom', true, null, this.viewContainerRef, ShowNotificationPopoverComponent, 'bottom', true, null,
{ {
@ -100,7 +100,9 @@ export class NotificationBellComponent implements OnDestroy {
private initSubscription() { private initSubscription() {
this.notificationSubscriber = NotificationSubscriber.createNotificationCountSubscription(this.notificationWsService, this.zone); this.notificationSubscriber = NotificationSubscriber.createNotificationCountSubscription(this.notificationWsService, this.zone);
this.notificationCountSubscriber = this.notificationSubscriber.notificationCount$.subscribe(value => this.countSubject.next(value)); this.notificationCountSubscriber = this.notificationSubscriber.notificationCount$.pipe(
skip(1),
).subscribe(value => this.countSubject.next(value));
this.notificationSubscriber.subscribe(); this.notificationSubscriber.subscribe();
} }
@ -108,5 +110,6 @@ export class NotificationBellComponent implements OnDestroy {
private unsubscribeSubscription() { private unsubscribeSubscription() {
this.notificationCountSubscriber.unsubscribe(); this.notificationCountSubscriber.unsubscribe();
this.notificationSubscriber.unsubscribe(); this.notificationSubscriber.unsubscribe();
this.notificationSubscriber = null;
} }
} }

8
ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts

@ -22,9 +22,9 @@ import { AppState } from '@core/core.state';
import { Notification, NotificationRequest } from '@shared/models/notification.models'; import { Notification, NotificationRequest } from '@shared/models/notification.models';
import { NotificationWebsocketService } from '@core/ws/notification-websocket.service'; import { NotificationWebsocketService } from '@core/ws/notification-websocket.service';
import { BehaviorSubject, Observable, ReplaySubject, Subscription } from 'rxjs'; import { BehaviorSubject, Observable, ReplaySubject, Subscription } from 'rxjs';
import { map, share, tap } from 'rxjs/operators'; import { map, share, skip, tap } from 'rxjs/operators';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { NotificationSubscriber } from '@shared/models/websocket/notification-ws.models'; import { NotificationSubscriber } from '@shared/models/telemetry/telemetry.models';
@Component({ @Component({
selector: 'tb-show-notification-popover', selector: 'tb-show-notification-popover',
@ -71,7 +71,9 @@ export class ShowNotificationPopoverComponent extends PageComponent implements O
}), }),
tap(() => setTimeout(() => this.cd.markForCheck())) tap(() => setTimeout(() => this.cd.markForCheck()))
); );
this.notificationCountSubscriber = this.notificationSubscriber.notificationCount$.subscribe(value => this.counter.next(value)); this.notificationCountSubscriber = this.notificationSubscriber.notificationCount$.pipe(
skip(1),
).subscribe(value => this.counter.next(value));
this.notificationSubscriber.subscribe(); this.notificationSubscriber.subscribe();
} }

4
ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.models.ts

@ -78,7 +78,7 @@ export const batteryLevelDefaultSettings: BatteryLevelWidgetSettings = {
}, },
valueColor: constantColor('rgba(0, 0, 0, 0.87)'), valueColor: constantColor('rgba(0, 0, 0, 0.87)'),
batteryLevelColor: { batteryLevelColor: {
color: 'rgba(92, 223, 144, 1)', color: 'rgba(224, 224, 224, 1)',
type: ColorType.range, type: ColorType.range,
rangeList: [ rangeList: [
{from: 0, to: 25, color: 'rgba(227, 71, 71, 1)'}, {from: 0, to: 25, color: 'rgba(227, 71, 71, 1)'},
@ -88,7 +88,7 @@ export const batteryLevelDefaultSettings: BatteryLevelWidgetSettings = {
colorFunction: defaultColorFunction colorFunction: defaultColorFunction
}, },
batteryShapeColor: { batteryShapeColor: {
color: 'rgba(92, 223, 144, 0.32)', color: 'rgba(224, 224, 224, 0.32)',
type: ColorType.range, type: ColorType.range,
rangeList: [ rangeList: [
{from: 0, to: 25, color: 'rgba(227, 71, 71, 0.32)'}, {from: 0, to: 25, color: 'rgba(227, 71, 71, 0.32)'},

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.html

@ -17,7 +17,7 @@
--> -->
<div class="tb-background-settings-panel" [formGroup]="backgroundSettingsFormGroup"> <div class="tb-background-settings-panel" [formGroup]="backgroundSettingsFormGroup">
<div class="tb-background-settings-title" translate>widgets.background.background-settings</div> <div class="tb-background-settings-title" translate>widgets.background.background-settings</div>
<div class="tb-form-panel"> <div class="tb-form-panel tb-background-form-panel">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<div class="tb-form-panel-title" translate>widgets.background.background</div> <div class="tb-form-panel-title" translate>widgets.background.background</div>
<tb-toggle-select formControlName="type" fxFlex selectMediaBreakpoint="xs"> <tb-toggle-select formControlName="type" fxFlex selectMediaBreakpoint="xs">
@ -28,7 +28,7 @@
</tb-toggle-select> </tb-toggle-select>
</div> </div>
<tb-gallery-image-input [fxShow]="backgroundSettingsFormGroup.get('type').value === backgroundType.image" formControlName="imageUrl"></tb-gallery-image-input> <tb-gallery-image-input [fxShow]="backgroundSettingsFormGroup.get('type').value === backgroundType.image" formControlName="imageUrl"></tb-gallery-image-input>
<div [fxShow]="backgroundSettingsFormGroup.get('type').value === backgroundType.color" class="tb-form-row space-between"> <div [fxShow]="backgroundSettingsFormGroup.get('type').value === backgroundType.color" class="tb-form-row space-between tb-background-color-field">
<div translate>widgets.color.color</div> <div translate>widgets.color.color</div>
<tb-color-input asBoxInput <tb-color-input asBoxInput
formControlName="color"> formControlName="color">

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.scss

@ -31,6 +31,12 @@
letter-spacing: 0.25px; letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87); color: rgba(0, 0, 0, 0.87);
} }
.tb-background-form-panel {
height: 192px;
.tb-background-color-field {
height: auto;
}
}
.tb-background-settings-preview { .tb-background-settings-preview {
flex: 1; flex: 1;
background: rgba(0, 0, 0, 0.04); background: rgba(0, 0, 0, 0.04);

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save