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",
"hasBasicMode": true,
"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,
"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.edge.EdgeEventService;
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.event.EventService;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor;
@ -455,6 +456,11 @@ public class ActorSystemContext {
@Getter
private WidgetTypeService widgetTypeService;
@Lazy
@Autowired(required = false)
@Getter
private EntityService entityService;
@Value("${actors.session.max_concurrent_sessions_per_device:1}")
@Getter
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.edge.EdgeEventService;
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.nosql.CassandraStatementTask;
import org.thingsboard.server.dao.nosql.TbResultSetFuture;
@ -896,6 +897,11 @@ class DefaultTbContext implements TbContext {
return mainCtx.getApiUsageStateService();
}
@Override
public EntityService getEntityService() {
return mainCtx.getEntityService();
}
private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) {
TbMsgMetaData metaData = new TbMsgMetaData();
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.configuration.EnableWebSecurity;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
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.filter.CorsFilter;
import org.springframework.web.filter.ShallowEtagHeaderFilter;
@ -63,7 +61,7 @@ import java.util.List;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Order(SecurityProperties.BASIC_AUTH_ORDER)
@TbCoreComponent
public class ThingsboardSecurityConfiguration {
@ -79,7 +77,7 @@ public class ThingsboardSecurityConfiguration {
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/**"};
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 DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT = "/api/device-connectivity/mqtts/certificate/download";
@ -115,10 +113,6 @@ public class ThingsboardSecurityConfiguration {
@Qualifier("jwtHeaderTokenExtractor")
private TokenExtractor jwtHeaderTokenExtractor;
@Autowired
@Qualifier("jwtQueryTokenExtractor")
private TokenExtractor jwtQueryTokenExtractor;
@Autowired private AuthenticationManager authenticationManager;
@Autowired private RateLimitProcessingFilter rateLimitProcessingFilter;
@ -150,7 +144,7 @@ public class ThingsboardSecurityConfiguration {
protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception {
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,
DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT));
SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT);
@ -167,15 +161,6 @@ public class ThingsboardSecurityConfiguration {
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
public AuthenticationManager authenticationManager(ObjectPostProcessor<Object> objectPostProcessor) throws Exception {
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
.and()
.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
.and()
.exceptionHandling().accessDeniedHandler(restAccessDeniedHandler)
@ -238,7 +223,6 @@ public class ThingsboardSecurityConfiguration {
.addFilterBefore(buildRestPublicLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class);
if (oauth2Configuration != null) {
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 org.springframework.context.annotation.Bean;
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.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
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.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.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.Map;
@Configuration
@TbCoreComponent
@ -46,8 +34,9 @@ import java.util.Map;
@Slf4j
public class WebSocketConfiguration implements WebSocketConfigurer {
public static final String WS_PLUGIN_PREFIX = "/api/ws/plugins/";
private static final String WS_PLUGIN_MAPPING = WS_PLUGIN_PREFIX + "**";
public static final String WS_API_ENDPOINT = "/api/ws";
public static final String WS_PLUGINS_ENDPOINT = "/api/ws/plugins/";
private static final String WS_API_MAPPING = "/api/ws/**";
private final WebSocketHandler wsHandler;
@ -65,39 +54,7 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
log.error("TbWebSocketHandler expected but [{}] provided", wsHandler);
throw new RuntimeException("TbWebSocketHandler expected but " + wsHandler + " provided");
}
registry.addHandler(wsHandler, WS_PLUGIN_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
}
});
registry.addHandler(wsHandler, WS_API_MAPPING).setAllowedOriginPatterns("*");
}
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.PageLink;
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.service.validator.ResourceDataValidator;
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.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
@ -77,6 +78,8 @@ public class ImageController extends BaseController {
private final ImageService imageService;
private final TbImageService tbImageService;
private final ResourceDataValidator resourceValidator;
@Value("${cache.image.systemImagesBrowserTtlInMinutes:0}")
private int systemImagesBrowserTtlInMinutes;
@Value("${cache.image.tenantImagesBrowserTtlInMinutes:0}")
@ -94,6 +97,7 @@ public class ImageController extends BaseController {
TbResource image = new TbResource();
image.setTenantId(user.getTenantId());
accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.CREATE, null, image);
resourceValidator.validateResourceSize(user.getTenantId(), null, file.getSize());
image.setFileName(file.getOriginalFilename());
if (StringUtils.isNotEmpty(title)) {
@ -115,6 +119,8 @@ public class ImageController extends BaseController {
@PathVariable String key,
@RequestPart MultipartFile file) throws Exception {
TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.WRITE);
resourceValidator.validateResourceSize(getTenantId(), imageInfo.getId(), file.getSize());
TbResource image = new TbResource(imageInfo);
image.setData(file.getBytes());
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;
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 org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.BeanCreationNotAllowedException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.CloseStatus;
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.adapter.NativeWebSocketSession;
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.exception.ThingsboardErrorCode;
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.util.limits.RateLimitService;
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.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.WebSocketMsgEndpoint;
import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.WebSocketSessionRef;
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.SendHandler;
import javax.websocket.SendResult;
import javax.websocket.Session;
import java.io.IOException;
import java.net.URI;
import java.security.InvalidParameterException;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
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.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;
@Service
@TbCoreComponent
@Slf4j
@RequiredArgsConstructor
public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocketMsgEndpoint {
private final ConcurrentMap<String, SessionMetaData> internalSessionMap = new ConcurrentHashMap<>();
private final ConcurrentMap<String, String> externalSessionMap = new ConcurrentHashMap<>();
@Autowired @Lazy
private WebSocketService webSocketService;
@Autowired
private TbTenantProfileCache tenantProfileCache;
@Autowired
private RateLimitService rateLimitService;
@Autowired
private JwtAuthenticationProvider authenticationProvider;
@Value("${server.ws.send_timeout:5000}")
private long sendTimeout;
@ -89,6 +105,8 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
private long pingTimeout;
@Value("${server.ws.max_queue_messages_per_session:1000}")
private int wsMaxQueueMessagesPerSession;
@Value("${server.ws.auth_timeout_ms:10000}")
private int authTimeoutMs;
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>> 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
public void handleTextMessage(WebSocketSession session, TextMessage message) {
try {
SessionMetaData sessionMd = internalSessionMap.get(session.getId());
if (sessionMd != null) {
log.trace("[{}][{}] Processing {}", sessionMd.sessionRef.getSecurityCtx().getTenantId(), session.getId(), message.getPayload());
webSocketService.handleWebSocketMsg(sessionMd.sessionRef, message.getPayload());
} else {
SessionMetaData sessionMd = getSessionMd(session.getId());
if (sessionMd == null) {
log.trace("[{}] Failed to find session", session.getId());
session.close(CloseStatus.SERVER_ERROR.withReason("Session not found!"));
return;
}
sessionMd.onMsg(message.getPayload());
} catch (IOException 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
protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {
try {
SessionMetaData sessionMd = internalSessionMap.get(session.getId());
SessionMetaData sessionMd = getSessionMd(session.getId());
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());
} else {
log.trace("[{}] Failed to find session", session.getId());
@ -139,23 +226,9 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
nativeSession.getAsyncRemote().setSendTimeout(sendTimeout);
}
}
String internalSessionId = session.getId();
WebSocketSessionRef sessionRef = toRef(session);
String externalSessionId = sessionRef.getSessionId();
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());
log.debug("[{}][{}] Session opened from address: {}", sessionRef.getSessionId(), session.getId(), session.getRemoteAddress());
establishSession(session, sessionRef, null);
} catch (InvalidParameterException e) {
log.warn("[{}] Failed to start session", session.getId(), e);
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
public void handleTransportError(WebSocketSession session, Throwable tError) throws Exception {
super.handleTransportError(session, tError);
SessionMetaData sessionMd = internalSessionMap.get(session.getId());
SessionMetaData sessionMd = getSessionMd(session.getId());
if (sessionMd != null) {
processInWebSocketService(sessionMd.sessionRef, SessionEvent.onError(tError));
} else {
@ -181,63 +280,87 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
super.afterConnectionClosed(session, closeStatus);
SessionMetaData sessionMd = internalSessionMap.remove(session.getId());
if (sessionMd == null) {
sessionMd = pendingSessions.asMap().remove(session.getId());
}
if (sessionMd != null) {
cleanupLimits(session, sessionMd.sessionRef);
externalSessionMap.remove(sessionMd.sessionRef.getSessionId());
processInWebSocketService(sessionMd.sessionRef, SessionEvent.onClosed());
log.info("[{}][{}][{}] Session is closed", sessionMd.sessionRef.getSecurityCtx().getTenantId(), sessionMd.sessionRef.getSessionId(), session.getId());
if (sessionMd.sessionRef.getSecurityCtx() != null) {
cleanupLimits(session, sessionMd.sessionRef);
processInWebSocketService(sessionMd.sessionRef, SessionEvent.onClosed());
}
log.info("{} Session is closed", sessionMd.sessionRef);
} else {
log.info("[{}] Session is closed", session.getId());
}
}
private void processInWebSocketService(WebSocketSessionRef sessionRef, SessionEvent event) {
if (sessionRef.getSecurityCtx() == null) {
return;
}
try {
webSocketService.handleWebSocketSessionEvent(sessionRef, event);
webSocketService.handleSessionEvent(sessionRef, event);
} 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 {
URI sessionUri = session.getUri();
String path = sessionUri.getPath();
path = path.substring(WebSocketConfiguration.WS_PLUGIN_PREFIX.length());
if (path.length() == 0) {
throw new IllegalArgumentException("URL should contain plugin token!");
private WebSocketSessionRef toRef(WebSocketSession session) {
String path = session.getUri().getPath();
WebSocketSessionType sessionType;
if (path.equals(WebSocketConfiguration.WS_API_ENDPOINT)) {
sessionType = WebSocketSessionType.GENERAL;
} 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()
.sessionId(UUID.randomUUID().toString())
.securityCtx(currentUser)
.securityCtx(securityCtx)
.localAddress(session.getLocalAddress())
.remoteAddress(session.getRemoteAddress())
.sessionType(sessionType)
.build();
}
private SessionMetaData getSessionMd(String internalSessionId) {
SessionMetaData sessionMd = internalSessionMap.get(internalSessionId);
if (sessionMd == null) {
sessionMd = pendingSessions.getIfPresent(internalSessionId);
}
return sessionMd;
}
class SessionMetaData implements SendHandler {
private final WebSocketSession session;
private final RemoteEndpoint.Async asyncRemote;
private final WebSocketSessionRef sessionRef;
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;
SessionMetaData(WebSocketSession session, WebSocketSessionRef sessionRef, int maxMsgQueuePerSession) {
SessionMetaData(WebSocketSession session, WebSocketSessionRef sessionRef) {
super();
this.session = session;
Session nativeSession = ((NativeWebSocketSession) session).getNativeSession(Session.class);
this.asyncRemote = nativeSession.getAsyncRemote();
this.sessionRef = sessionRef;
this.msgQueue = new LinkedBlockingQueue<>(maxMsgQueuePerSession);
this.lastActivityTime = System.currentTimeMillis();
}
@ -245,13 +368,13 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
try {
long timeSinceLastActivity = currentTime - lastActivityTime;
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);
} else if (timeSinceLastActivity >= pingTimeout / NUMBER_OF_PING_ATTEMPTS) {
sendMsg(TbWebSocketPingMsg.INSTANCE);
}
} 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);
}
}
@ -260,9 +383,9 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
try {
close(this.sessionRef, reason);
} catch (IOException ioe) {
log.trace("[{}] Session transport error", session.getId(), ioe);
log.trace("{} Session transport error", sessionRef, ioe);
} finally {
msgQueue.clear();
outboundMsgQueue.clear();
}
}
@ -275,19 +398,14 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
}
void sendMsg(TbWebSocketMsg<?> msg) {
try {
msgQueue.add(msg);
} catch (RuntimeException e) {
if (log.isTraceEnabled()) {
log.trace("[{}][{}] Session closed due to queue error", sessionRef.getSecurityCtx().getTenantId(), session.getId(), e);
} else {
log.info("[{}][{}] Session closed due to queue error", sessionRef.getSecurityCtx().getTenantId(), session.getId());
}
if (outboundMsgQueueSize.get() < maxMsgQueueSize) {
outboundMsgQueue.add(msg);
outboundMsgQueueSize.incrementAndGet();
processNextMsg();
} else {
log.info("{} Session closed due to updates queue size exceeded", sessionRef);
closeSession(CloseStatus.POLICY_VIOLATION.withReason("Max pending updates limit reached!"));
return;
}
processNextMsg();
}
private void sendMsgInternal(TbWebSocketMsg<?> msg) {
@ -303,7 +421,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
processNextMsg();
}
} 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);
}
}
@ -311,7 +429,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
@Override
public void onResult(SendResult result) {
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);
return;
}
@ -321,22 +439,45 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
}
private void processNextMsg() {
if (msgQueue.isEmpty() || !isSending.compareAndSet(false, true)) {
if (outboundMsgQueue.isEmpty() || !isSending.compareAndSet(false, true)) {
return;
}
TbWebSocketMsg<?> msg = msgQueue.poll();
TbWebSocketMsg<?> msg = outboundMsgQueue.poll();
if (msg != null) {
outboundMsgQueueSize.decrementAndGet();
sendMsgInternal(msg);
} else {
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
public void send(WebSocketSessionRef sessionRef, int subscriptionId, String msg) throws IOException {
log.debug("{} Sending {}", sessionRef, msg);
String externalId = sessionRef.getSessionId();
log.debug("[{}] Processing {}", externalId, msg);
String internalId = externalSessionMap.get(externalId);
if (internalId != null) {
SessionMetaData sessionMd = internalSessionMap.get(internalId);
@ -344,13 +485,12 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
TenantId tenantId = sessionRef.getSecurityCtx().getTenantId();
if (!rateLimitService.checkRateLimit(LimitedApi.WS_UPDATES_PER_SESSION, tenantId, (Object) sessionRef.getSessionId())) {
if (blacklistedSessions.putIfAbsent(externalId, sessionRef) == null) {
log.info("[{}][{}][{}] Failed to process session update. Max session updates limit reached"
, tenantId, sessionRef.getSecurityCtx().getId(), externalId);
log.info("{} Failed to process session update. Max session updates limit reached", sessionRef);
sessionMd.sendMsg("{\"subscriptionId\":" + subscriptionId + ", \"errorCode\":" + ThingsboardErrorCode.TOO_MANY_UPDATES.getErrorCode() + ", \"errorMsg\":\"Too many updates!\"}");
}
return;
} else {
log.debug("[{}][{}][{}] Session is no longer blacklisted.", tenantId, sessionRef.getSecurityCtx().getId(), externalId);
log.debug("{} Session is no longer blacklisted.", sessionRef);
blacklistedSessions.remove(externalId);
}
sessionMd.sendMsg(msg);
@ -381,10 +521,10 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
@Override
public void close(WebSocketSessionRef sessionRef, CloseStatus reason) throws IOException {
String externalId = sessionRef.getSessionId();
log.debug("[{}] Processing close request", externalId);
log.debug("{} Processing close request", sessionRef.toString());
String internalId = externalSessionMap.get(externalId);
if (internalId != null) {
SessionMetaData sessionMd = internalSessionMap.get(internalId);
SessionMetaData sessionMd = getSessionMd(internalId);
if (sessionMd != null) {
sessionMd.session.close(reason);
} 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);
if (tenantProfileConfiguration == null) {
return true;
@ -411,10 +551,9 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
}
}
if (!limitAllowed) {
log.info("[{}][{}][{}] Failed to start session. Max tenant sessions limit reached"
, sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId);
session.close(CloseStatus.POLICY_VIOLATION.withReason("Max tenant sessions limit reached!"));
return false;
log.info("{} Failed to start session. Max tenant sessions limit reached", sessionRef.toString());
session.close(CloseStatus.POLICY_VIOLATION.withReason("Max tenant sessions limit reached!"));
return false;
}
}
@ -428,10 +567,9 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
}
}
if (!limitAllowed) {
log.info("[{}][{}][{}] Failed to start session. Max customer sessions limit reached"
, sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId);
session.close(CloseStatus.POLICY_VIOLATION.withReason("Max customer sessions limit reached"));
return false;
log.info("{} Failed to start session. Max customer sessions limit reached", sessionRef.toString());
session.close(CloseStatus.POLICY_VIOLATION.withReason("Max customer sessions limit reached"));
return false;
}
}
if (tenantProfileConfiguration.getMaxWsSessionsPerRegularUser() > 0
@ -444,10 +582,9 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
}
}
if (!limitAllowed) {
log.info("[{}][{}][{}] Failed to start session. Max regular user sessions limit reached"
, sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId);
session.close(CloseStatus.POLICY_VIOLATION.withReason("Max regular user sessions limit reached"));
return false;
log.info("{} Failed to start session. Max regular user sessions limit reached", sessionRef.toString());
session.close(CloseStatus.POLICY_VIOLATION.withReason("Max regular user sessions limit reached"));
return false;
}
}
if (tenantProfileConfiguration.getMaxWsSessionsPerPublicUser() > 0
@ -460,10 +597,9 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
}
}
if (!limitAllowed) {
log.info("[{}][{}][{}] Failed to start session. Max public user sessions limit reached"
, sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId);
session.close(CloseStatus.POLICY_VIOLATION.withReason("Max public user sessions limit reached"));
return false;
log.info("{} Failed to start session. Max public user sessions limit reached", sessionRef.toString());
session.close(CloseStatus.POLICY_VIOLATION.withReason("Max public user sessions limit reached"));
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.JwtExpiredTokenException;
import org.thingsboard.server.service.security.exception.UserPasswordExpiredException;
import org.thingsboard.server.service.security.exception.UserPasswordNotValidException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
@ -191,6 +192,9 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
UserPasswordExpiredException expiredException = (UserPasswordExpiredException) authenticationException;
String resetToken = expiredException.getResetToken();
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 {
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":
log.info("Upgrading ThingsBoard from version 3.6.1 to 3.6.2 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.6.1");
installScripts.loadSystemImages();
if (!getEnv("SKIP_IMAGES_MIGRATION", false)) {
installScripts.updateImages();
installScripts.setUpdateImages(true);
} else {
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();
systemDataLoaderService.loadSystemWidgets();
installScripts.loadSystemLwm2mResources();
installScripts.loadSystemImages();
if (installScripts.isUpdateImages()) {
installScripts.updateImages();
}
}
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;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@ -113,6 +115,8 @@ public class InstallScripts {
@Autowired
private ImagesUpdater imagesUpdater;
@Getter @Setter
private boolean updateImages = false;
Path getTenantRuleChainsDir() {
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.Optional;
import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTINGS_CACHE;
@RequiredArgsConstructor
@Service
@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");
clearAll();
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:
//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);
if (result.isSuccess()) {
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;
} 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.id.UserId;
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 java.util.Optional;
@ -49,7 +48,7 @@ public class DefaultTokenOutdatingService implements TokenOutdatingService {
}
@Override
public boolean isOutdated(JwtToken token, UserId userId) {
public boolean isOutdated(String token, UserId userId) {
Claims claims = tokenFactory.parseTokenClaims(token).getBody();
long issueTime = claims.getIssuedAt().getTime();
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;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.model.JwtToken;
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;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
@ -37,13 +39,19 @@ public class JwtAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
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");
}
return new JwtAuthenticationToken(securityUser);
return securityUser;
}
@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 {
Assert.notNull(authentication, "No authentication data provided");
RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials();
SecurityUser unsafeUser = tokenFactory.parseRefreshToken(rawAccessToken);
SecurityUser unsafeUser = tokenFactory.parseRefreshToken(rawAccessToken.getToken());
UserPrincipal principal = unsafeUser.getUserPrincipal();
SecurityUser securityUser;
@ -67,7 +67,7 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide
securityUser = authenticateByPublicId(principal.getValue());
}
securityUser.setSessionId(unsafeUser.getSessionId());
if (tokenOutdatingService.isOutdated(rawAccessToken, securityUser.getId())) {
if (tokenOutdatingService.isOutdated(rawAccessToken.getToken(), securityUser.getId())) {
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;
import org.springframework.security.core.AuthenticationException;
import org.thingsboard.server.common.data.security.model.JwtToken;
public class JwtExpiredTokenException extends AuthenticationException {
private static final long serialVersionUID = -5959543783324224864L;
private JwtToken token;
private String token;
public JwtExpiredTokenException(String msg) {
super(msg);
}
public JwtExpiredTokenException(JwtToken token, String msg, Throwable t) {
public JwtExpiredTokenException(String token, String msg, Throwable t) {
super(msg, t);
this.token = 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);
}
public SecurityUser parseAccessJwtToken(RawAccessJwtToken rawAccessToken) {
Jws<Claims> jwsClaims = parseTokenClaims(rawAccessToken);
public SecurityUser parseAccessJwtToken(String token) {
Jws<Claims> jwsClaims = parseTokenClaims(token);
Claims claims = jwsClaims.getBody();
String subject = claims.getSubject();
@SuppressWarnings("unchecked")
@ -145,8 +145,8 @@ public class JwtTokenFactory {
return new AccessJwtToken(token);
}
public SecurityUser parseRefreshToken(RawAccessJwtToken rawAccessToken) {
Jws<Claims> jwsClaims = parseTokenClaims(rawAccessToken);
public SecurityUser parseRefreshToken(String token) {
Jws<Claims> jwsClaims = parseTokenClaims(token);
Claims claims = jwsClaims.getBody();
String subject = claims.getSubject();
@SuppressWarnings("unchecked")
@ -200,11 +200,11 @@ public class JwtTokenFactory {
.signWith(SignatureAlgorithm.HS512, jwtSettingsService.getJwtSettings().getTokenSigningKey());
}
public Jws<Claims> parseTokenClaims(JwtToken token) {
public Jws<Claims> parseTokenClaims(String token) {
try {
return Jwts.parser()
.setSigningKey(jwtSettingsService.getJwtSettings().getTokenSigningKey())
.parseClaimsJws(token.getToken());
.parseClaimsJws(token);
} catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException ex) {
log.debug("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.service.security.auth.rest.RestAuthenticationDetails;
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.utils.MiscUtils;
import ua_parser.Client;
@ -107,6 +108,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
securitySettings = new SecuritySettings();
securitySettings.setPasswordPolicy(new UserPasswordPolicy());
securitySettings.getPasswordPolicy().setMinimumLength(6);
securitySettings.getPasswordPolicy().setMaximumLength(72);
}
return securitySettings;
}
@ -131,9 +133,19 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
@Override
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())) {
int failedLoginAttempts = userService.increaseFailedLoginAttempts(tenantId, userCredentials.getUserId());
SecuritySettings securitySettings = self.getSecuritySettings(tenantId);
if (securitySettings.getMaxFailedLoginAttempts() != null && securitySettings.getMaxFailedLoginAttempts() > 0) {
if (failedLoginAttempts > securitySettings.getMaxFailedLoginAttempts() && userCredentials.isEnabled()) {
lockAccount(userCredentials.getUserId(), username, securitySettings.getUserLockoutNotificationEmail(), securitySettings.getMaxFailedLoginAttempts());
@ -149,7 +161,6 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
userService.resetFailedLoginAttempts(tenantId, userCredentials.getUserId());
SecuritySettings securitySettings = self.getSecuritySettings(tenantId);
if (isPositiveInteger(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays())) {
if ((userCredentials.getCreatedTime()
+ TimeUnit.DAYS.toMillis(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays()))
@ -199,8 +210,31 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
SecuritySettings securitySettings = self.getSecuritySettings(tenantId);
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<>();
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())) {
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));
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

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) {
wsLock.lock();
try {
wsService.sendWsMsg(sessionRef.getSessionId(), update);
wsService.sendUpdate(sessionRef.getSessionId(), update);
} finally {
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 java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
@Data
@ -35,6 +36,8 @@ public abstract class TbSubscription<T> {
private final TbSubscriptionType type;
private final BiConsumer<TbSubscription<T>, T> updateProcessor;
protected final AtomicInteger sequence = new AtomicInteger();
@Override
public boolean equals(Object o) {
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;
protected static final List<EntityType> SUPPORTED_ENTITY_TYPES = List.of(
EntityType.CUSTOMER, EntityType.RULE_CHAIN, EntityType.DASHBOARD,
EntityType.ASSET_PROFILE, EntityType.ASSET,
EntityType.CUSTOMER, EntityType.RULE_CHAIN, EntityType.TB_RESOURCE,
EntityType.DASHBOARD, EntityType.ASSET_PROFILE, EntityType.ASSET,
EntityType.DEVICE_PROFILE, EntityType.DEVICE,
EntityType.ENTITY_VIEW, EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE,
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.setDefaultRuleChainId(getExternalIdOrElseInternal(ctx, assetProfile.getDefaultRuleChainId()));
assetProfile.setDefaultEdgeRuleChainId(getExternalIdOrElseInternal(ctx, assetProfile.getDefaultEdgeRuleChainId()));
imageService.inlineImage(assetProfile);
}
@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()) {
replaceUuidsRecursively(ctx, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Collections.emptySet(), WIDGET_CONFIG_PROCESSED_FIELDS_PATTERN);
}
imageService.inlineImages(dashboard);
}
@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.dao.attributes.AttributesService;
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.service.sync.ie.exporting.EntityExportService;
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;
@Autowired
private AttributesService attributesService;
@Autowired
protected ImageService imageService;
@Override
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.setDefaultRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultRuleChainId()));
deviceProfile.setDefaultEdgeRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultEdgeRuleChainId()));
imageService.inlineImage(deviceProfile);
}
@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> {
@Override
protected void setRelatedEntities(EntitiesExportCtx<?> ctx, WidgetTypeDetails widgetsBundle, WidgetTypeExportData exportData) {
if (widgetsBundle.getTenantId() == null || widgetsBundle.getTenantId().isNullUid()) {
protected void setRelatedEntities(EntitiesExportCtx<?> ctx, WidgetTypeDetails widgetTypeDetails, WidgetTypeExportData exportData) {
if (widgetTypeDetails.getTenantId() == null || widgetTypeDetails.getTenantId().isNullUid()) {
throw new IllegalArgumentException("Export of system Widget Type is not allowed");
}
imageService.inlineImages(widgetTypeDetails);
}
@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()) {
throw new IllegalArgumentException("Export of system Widget Bundles is not allowed");
}
imageService.inlineImage(widgetsBundle);
List<String> fqns = widgetTypeService.findWidgetFqnsByWidgetsBundleId(ctx.getTenantId(), widgetsBundle.getId());
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.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
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.TbTimeSeriesSubscription;
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.GetHistoryCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v1.SubscriptionCmd;
@ -87,6 +86,7 @@ import javax.annotation.PreDestroy;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
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_ATTRIBUTES = "Failed to fetch attributes!";
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<>();
@ -149,8 +148,7 @@ public class DefaultWebSocketService implements WebSocketService {
private ScheduledExecutorService pingExecutor;
private String serviceId;
private List<WsCmdListHandler<TelemetryPluginCmdsWrapper, ?>> telemetryCmdsHandlers;
private List<WsCmdHandler<NotificationCmdsWrapper, ? extends WsCmd>> notificationCmdsHandlers;
private Map<WsCmdType, WsCmdHandler<? extends WsCmd>> cmdsHandlers;
@PostConstruct
public void init() {
@ -160,26 +158,23 @@ public class DefaultWebSocketService implements WebSocketService {
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);
telemetryCmdsHandlers = List.of(
newCmdsHandler(TelemetryPluginCmdsWrapper::getAttrSubCmds, this::handleWsAttributesSubscriptionCmd),
newCmdsHandler(TelemetryPluginCmdsWrapper::getTsSubCmds, this::handleWsTimeseriesSubscriptionCmd),
newCmdsHandler(TelemetryPluginCmdsWrapper::getHistoryCmds, this::handleWsHistoryCmd),
newCmdsHandler(TelemetryPluginCmdsWrapper::getEntityDataCmds, this::handleWsEntityDataCmd),
newCmdsHandler(TelemetryPluginCmdsWrapper::getAlarmDataCmds, this::handleWsAlarmDataCmd),
newCmdsHandler(TelemetryPluginCmdsWrapper::getEntityCountCmds, this::handleWsEntityCountCmd),
newCmdsHandler(TelemetryPluginCmdsWrapper::getAlarmCountCmds, this::handleWsAlarmCountCmd),
newCmdsHandler(TelemetryPluginCmdsWrapper::getEntityDataUnsubscribeCmds, this::handleWsDataUnsubscribeCmd),
newCmdsHandler(TelemetryPluginCmdsWrapper::getAlarmDataUnsubscribeCmds, this::handleWsDataUnsubscribeCmd),
newCmdsHandler(TelemetryPluginCmdsWrapper::getEntityCountUnsubscribeCmds, this::handleWsDataUnsubscribeCmd),
newCmdsHandler(TelemetryPluginCmdsWrapper::getAlarmCountUnsubscribeCmds, this::handleWsDataUnsubscribeCmd)
);
notificationCmdsHandlers = List.of(
newCmdHandler(NotificationCmdsWrapper::getUnreadSubCmd, notificationCmdsHandler::handleUnreadNotificationsSubCmd),
newCmdHandler(NotificationCmdsWrapper::getUnreadCountSubCmd, notificationCmdsHandler::handleUnreadNotificationsCountSubCmd),
newCmdHandler(NotificationCmdsWrapper::getMarkAsReadCmd, notificationCmdsHandler::handleMarkAsReadCmd),
newCmdHandler(NotificationCmdsWrapper::getMarkAllAsReadCmd, notificationCmdsHandler::handleMarkAllAsReadCmd),
newCmdHandler(NotificationCmdsWrapper::getUnsubCmd, notificationCmdsHandler::handleUnsubCmd)
);
cmdsHandlers = new EnumMap<>(WsCmdType.class);
cmdsHandlers.put(WsCmdType.ATTRIBUTES, newCmdHandler(this::handleWsAttributesSubscriptionCmd));
cmdsHandlers.put(WsCmdType.TIMESERIES, newCmdHandler(this::handleWsTimeseriesSubscriptionCmd));
cmdsHandlers.put(WsCmdType.TIMESERIES_HISTORY, newCmdHandler(this::handleWsHistoryCmd));
cmdsHandlers.put(WsCmdType.ENTITY_DATA, newCmdHandler(this::handleWsEntityDataCmd));
cmdsHandlers.put(WsCmdType.ALARM_DATA, newCmdHandler(this::handleWsAlarmDataCmd));
cmdsHandlers.put(WsCmdType.ENTITY_COUNT, newCmdHandler(this::handleWsEntityCountCmd));
cmdsHandlers.put(WsCmdType.ALARM_COUNT, newCmdHandler(this::handleWsAlarmCountCmd));
cmdsHandlers.put(WsCmdType.ENTITY_DATA_UNSUBSCRIBE, newCmdHandler(this::handleWsDataUnsubscribeCmd));
cmdsHandlers.put(WsCmdType.ALARM_DATA_UNSUBSCRIBE, newCmdHandler(this::handleWsDataUnsubscribeCmd));
cmdsHandlers.put(WsCmdType.ENTITY_COUNT_UNSUBSCRIBE, newCmdHandler(this::handleWsDataUnsubscribeCmd));
cmdsHandlers.put(WsCmdType.ALARM_COUNT_UNSUBSCRIBE, newCmdHandler(this::handleWsDataUnsubscribeCmd));
cmdsHandlers.put(WsCmdType.NOTIFICATIONS, newCmdHandler(notificationCmdsHandler::handleUnreadNotificationsSubCmd));
cmdsHandlers.put(WsCmdType.NOTIFICATIONS_COUNT, newCmdHandler(notificationCmdsHandler::handleUnreadNotificationsCountSubCmd));
cmdsHandlers.put(WsCmdType.MARK_NOTIFICATIONS_AS_READ, newCmdHandler(notificationCmdsHandler::handleMarkAsReadCmd));
cmdsHandlers.put(WsCmdType.MARK_ALL_NOTIFICATIONS_AS_READ, newCmdHandler(notificationCmdsHandler::handleMarkAllAsReadCmd));
cmdsHandlers.put(WsCmdType.NOTIFICATIONS_UNSUBSCRIBE, newCmdHandler(notificationCmdsHandler::handleUnsubCmd));
}
@PreDestroy
@ -194,7 +189,7 @@ public class DefaultWebSocketService implements WebSocketService {
}
@Override
public void handleWebSocketSessionEvent(WebSocketSessionRef sessionRef, SessionEvent event) {
public void handleSessionEvent(WebSocketSessionRef sessionRef, SessionEvent event) {
String sessionId = sessionRef.getSessionId();
log.debug(PROCESSING_MSG, sessionId, event);
switch (event.getEventType()) {
@ -214,120 +209,75 @@ public class DefaultWebSocketService implements WebSocketService {
}
@Override
public void handleWebSocketMsg(WebSocketSessionRef sessionRef, String msg) {
if (log.isTraceEnabled()) {
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) {
public void handleCommands(WebSocketSessionRef sessionRef, WsCommandsWrapper commandsWrapper) {
if (commandsWrapper == null || CollectionUtils.isEmpty(commandsWrapper.getCmds())) {
return;
}
for (WsCmdListHandler<TelemetryPluginCmdsWrapper, ?> cmdHandler : telemetryCmdsHandlers) {
List<?> cmds = cmdHandler.extractCmds(cmdsWrapper);
if (cmds != null) {
cmdHandler.handle(sessionRef, cmds);
}
String sessionId = sessionRef.getSessionId();
if (!validateSessionMetadata(sessionRef, UNKNOWN_SUBSCRIPTION_ID, sessionId)) {
return;
}
}
private void processNotificationCmds(WebSocketSessionRef sessionRef, String msg) throws IOException {
NotificationCmdsWrapper cmdsWrapper = JacksonUtil.fromString(msg, NotificationCmdsWrapper.class);
for (WsCmdHandler<NotificationCmdsWrapper, ? extends WsCmd> cmdHandler : notificationCmdsHandlers) {
WsCmd cmd = cmdHandler.extractCmd(cmdsWrapper);
if (cmd != null) {
String sessionId = sessionRef.getSessionId();
if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)) {
try {
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);
}
}
for (WsCmd cmd : commandsWrapper.getCmds()) {
log.debug("[{}][{}][{}] Processing cmd: {}", sessionId, cmd.getType(), cmd.getCmdId(), cmd);
try {
Optional.ofNullable(cmdsHandlers.get(cmd.getType()))
.ifPresent(cmdHandler -> 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) {
String sessionId = sessionRef.getSessionId();
log.debug("[{}] Processing: {}", sessionId, cmd);
if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)
&& validateSubscriptionCmd(sessionRef, cmd)) {
if (validateSubscriptionCmd(sessionRef, cmd)) {
entityDataSubService.handleCmd(sessionRef, cmd);
}
}
private void handleWsEntityCountCmd(WebSocketSessionRef sessionRef, EntityCountCmd cmd) {
String sessionId = sessionRef.getSessionId();
log.debug("[{}] Processing: {}", sessionId, cmd);
if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)
&& validateSubscriptionCmd(sessionRef, cmd)) {
if (validateSubscriptionCmd(sessionRef, cmd)) {
entityDataSubService.handleCmd(sessionRef, cmd);
}
}
private void handleWsAlarmDataCmd(WebSocketSessionRef sessionRef, AlarmDataCmd cmd) {
String sessionId = sessionRef.getSessionId();
log.debug("[{}] Processing: {}", sessionId, cmd);
if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)
&& validateSubscriptionCmd(sessionRef, cmd)) {
if (validateSubscriptionCmd(sessionRef, cmd)) {
entityDataSubService.handleCmd(sessionRef, cmd);
}
}
private void handleWsDataUnsubscribeCmd(WebSocketSessionRef sessionRef, UnsubscribeCmd cmd) {
String sessionId = sessionRef.getSessionId();
log.debug("[{}] Processing: {}", sessionId, cmd);
if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)) {
entityDataSubService.cancelSubscription(sessionRef.getSessionId(), cmd);
}
entityDataSubService.cancelSubscription(sessionRef.getSessionId(), cmd);
}
private void handleWsAlarmCountCmd(WebSocketSessionRef sessionRef, AlarmCountCmd cmd) {
String sessionId = sessionRef.getSessionId();
log.debug("[{}] Processing: {}", sessionId, cmd);
if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)
&& validateSubscriptionCmd(sessionRef, cmd)) {
if (validateCmd(sessionRef, cmd)) {
entityDataSubService.handleCmd(sessionRef, cmd);
}
}
@Override
public void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate update) {
sendWsMsg(sessionId, update.getSubscriptionId(), update);
public void sendUpdate(String sessionId, TelemetrySubscriptionUpdate update) {
sendUpdate(sessionId, update.getSubscriptionId(), update);
}
@Override
public void sendUpdate(String sessionId, CmdUpdate update) {
sendUpdate(sessionId, update.getCmdId(), update);
}
@Override
public void sendWsMsg(String sessionId, CmdUpdate update) {
sendWsMsg(sessionId, update.getCmdId(), update);
public void sendError(WebSocketSessionRef sessionRef, int subId, SubscriptionErrorCode errorCode, String errorMsg) {
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);
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();
log.debug("[{}] Processing: {}", sessionId, cmd);
if (validateSessionMetadata(sessionRef, cmd, sessionId)) {
if (cmd.isUnsubscribe()) {
unsubscribe(sessionRef, cmd, sessionId);
} else if (validateSubscriptionCmd(sessionRef, cmd)) {
EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId());
log.debug("[{}] fetching latest attributes ({}) values for device: {}", sessionId, cmd.getKeys(), entityId);
Optional<Set<String>> keysOptional = getKeys(cmd);
if (keysOptional.isPresent()) {
List<String> keys = new ArrayList<>(keysOptional.get());
handleWsAttributesSubscriptionByKeys(sessionRef, cmd, sessionId, entityId, keys);
} else {
handleWsAttributesSubscription(sessionRef, cmd, sessionId, entityId);
}
if (cmd.isUnsubscribe()) {
unsubscribe(sessionRef, cmd, sessionId);
} else if (validateSubscriptionCmd(sessionRef, cmd)) {
EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId());
log.debug("[{}] fetching latest attributes ({}) values for device: {}", sessionId, cmd.getKeys(), entityId);
Optional<Set<String>> keysOptional = getKeys(cmd);
if (keysOptional.isPresent()) {
List<String> keys = new ArrayList<>(keysOptional.get());
handleWsAttributesSubscriptionByKeys(sessionRef, cmd, sessionId, entityId, keys);
} else {
handleWsAttributesSubscription(sessionRef, cmd, sessionId, entityId);
}
}
}
@ -503,7 +449,7 @@ public class DefaultWebSocketService implements WebSocketService {
.updateProcessor((subscription, update) -> {
subLock.lock();
try {
sendWsMsg(subscription.getSessionId(), update);
sendUpdate(subscription.getSessionId(), update);
} finally {
subLock.unlock();
}
@ -511,9 +457,9 @@ public class DefaultWebSocketService implements WebSocketService {
.build();
subLock.lock();
try{
try {
oldSubService.addSubscription(sub);
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData));
sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData));
} finally {
subLock.unlock();
}
@ -531,7 +477,7 @@ public class DefaultWebSocketService implements WebSocketService {
update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
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) {
String sessionId = sessionRef.getSessionId();
WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId);
if (sessionMD == null) {
log.warn("[{}] Session meta data not found. ", sessionId);
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
SESSION_META_DATA_NOT_FOUND);
sendWsMsg(sessionRef, update);
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;
}
if (!validateCmd(sessionRef, cmd, () -> {
if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty() || cmd.getEntityType() == null || cmd.getEntityType().isEmpty()) {
throw new IllegalArgumentException("Device id is empty!");
}
if (cmd.getKeys() == null || cmd.getKeys().isEmpty()) {
throw new IllegalArgumentException("Keys are empty!");
}
})) return;
EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId());
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())))
@ -572,7 +506,7 @@ public class DefaultWebSocketService implements WebSocketService {
FutureCallback<List<TsKvEntry>> callback = new FutureCallback<List<TsKvEntry>>() {
@Override
public void onSuccess(List<TsKvEntry> data) {
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data));
sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data));
}
@Override
@ -585,7 +519,7 @@ public class DefaultWebSocketService implements WebSocketService {
update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
FAILED_TO_FETCH_DATA);
}
sendWsMsg(sessionRef, update);
sendUpdate(sessionRef, update);
}
};
accessValidator.validate(sessionRef.getSecurityCtx(), Operation.READ_TELEMETRY, entityId,
@ -620,7 +554,7 @@ public class DefaultWebSocketService implements WebSocketService {
.updateProcessor((subscription, update) -> {
subLock.lock();
try {
sendWsMsg(subscription.getSessionId(), update);
sendUpdate(subscription.getSessionId(), update);
} finally {
subLock.unlock();
}
@ -631,7 +565,7 @@ public class DefaultWebSocketService implements WebSocketService {
subLock.lock();
try {
oldSubService.addSubscription(sub);
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData));
sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData));
} finally {
subLock.unlock();
}
@ -640,9 +574,7 @@ public class DefaultWebSocketService implements WebSocketService {
@Override
public void onFailure(Throwable e) {
log.error(FAILED_TO_FETCH_ATTRIBUTES, e);
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
FAILED_TO_FETCH_ATTRIBUTES);
sendWsMsg(sessionRef, update);
sendError(sessionRef, cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, FAILED_TO_FETCH_ATTRIBUTES);
}
};
@ -660,20 +592,16 @@ public class DefaultWebSocketService implements WebSocketService {
}
String sessionId = sessionRef.getSessionId();
log.debug("[{}] Processing: {}", sessionId, cmd);
if (validateSessionMetadata(sessionRef, cmd, sessionId)) {
if (cmd.isUnsubscribe()) {
unsubscribe(sessionRef, cmd, sessionId);
} else if (validateSubscriptionCmd(sessionRef, cmd)) {
EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId());
Optional<Set<String>> keysOptional = getKeys(cmd);
if (keysOptional.isPresent()) {
handleWsTimeSeriesSubscriptionByKeys(sessionRef, cmd, sessionId, entityId);
} else {
handleWsTimeSeriesSubscription(sessionRef, cmd, sessionId, entityId);
}
if (cmd.isUnsubscribe()) {
unsubscribe(sessionRef, cmd, sessionId);
} else if (validateSubscriptionCmd(sessionRef, cmd)) {
EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId());
Optional<Set<String>> keysOptional = getKeys(cmd);
if (keysOptional.isPresent()) {
handleWsTimeSeriesSubscriptionByKeys(sessionRef, cmd, sessionId, entityId);
} else {
handleWsTimeSeriesSubscription(sessionRef, cmd, sessionId, entityId);
}
}
}
@ -721,7 +649,7 @@ public class DefaultWebSocketService implements WebSocketService {
.updateProcessor((subscription, update) -> {
subLock.lock();
try {
sendWsMsg(subscription.getSessionId(), update);
sendUpdate(subscription.getSessionId(), update);
} finally {
subLock.unlock();
}
@ -734,7 +662,7 @@ public class DefaultWebSocketService implements WebSocketService {
subLock.lock();
try {
oldSubService.addSubscription(sub);
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data));
sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data));
} finally {
subLock.unlock();
}
@ -750,7 +678,7 @@ public class DefaultWebSocketService implements WebSocketService {
update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
FAILED_TO_FETCH_DATA);
}
sendWsMsg(sessionRef, update);
sendUpdate(sessionRef, update);
}
};
accessValidator.validate(sessionRef.getSecurityCtx(), Operation.READ_TELEMETRY, entityId,
@ -776,7 +704,7 @@ public class DefaultWebSocketService implements WebSocketService {
.updateProcessor((subscription, update) -> {
subLock.lock();
try {
sendWsMsg(subscription.getSessionId(), update);
sendUpdate(subscription.getSessionId(), update);
} finally {
subLock.unlock();
}
@ -787,9 +715,9 @@ public class DefaultWebSocketService implements WebSocketService {
.build();
subLock.lock();
try{
try {
oldSubService.addSubscription(sub);
sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data));
sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data));
} finally {
subLock.unlock();
}
@ -802,9 +730,7 @@ public class DefaultWebSocketService implements WebSocketService {
} else {
log.info(FAILED_TO_FETCH_DATA, e);
}
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
FAILED_TO_FETCH_DATA);
sendWsMsg(sessionRef, update);
sendError(sessionRef, cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, FAILED_TO_FETCH_DATA);
}
};
}
@ -818,95 +744,77 @@ public class DefaultWebSocketService implements WebSocketService {
}
private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, EntityDataCmd cmd) {
if (cmd.getCmdId() < 0) {
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
"Cmd id is negative value!");
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;
return validateCmd(sessionRef, cmd, () -> {
if (cmd.getQuery() == null && !cmd.hasAnyCmd()) {
throw new IllegalArgumentException("Query is empty!");
}
});
}
private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, EntityCountCmd cmd) {
if (cmd.getCmdId() < 0) {
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
"Cmd id is negative value!");
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;
return validateCmd(sessionRef, cmd, () -> {
if (cmd.getQuery() == null) {
throw new IllegalArgumentException("Query is empty!");
}
});
}
private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, AlarmDataCmd cmd) {
if (cmd.getCmdId() < 0) {
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
"Cmd id is negative value!");
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;
return validateCmd(sessionRef, cmd, () -> {
if (cmd.getQuery() == null) {
throw new IllegalArgumentException("Query is empty!");
}
});
}
private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, SubscriptionCmd cmd) {
if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) {
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
"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);
return validateCmd(sessionRef, cmd, () -> {
if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) {
throw new IllegalArgumentException("Device id is empty!");
}
});
}
private boolean validateSessionMetadata(WebSocketSessionRef sessionRef, int cmdId, String sessionId) {
WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId);
if (sessionMD == null) {
log.warn("[{}] Session meta data not found. ", sessionId);
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmdId, SubscriptionErrorCode.INTERNAL_ERROR,
SESSION_META_DATA_NOT_FOUND);
sendWsMsg(sessionRef, update);
sendError(sessionRef, cmdId, SubscriptionErrorCode.INTERNAL_ERROR, SESSION_META_DATA_NOT_FOUND);
return false;
} else {
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) {
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
"Cmd id is negative value!");
sendWsMsg(sessionRef, update);
sendError(sessionRef, cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, "Cmd id is negative value!");
return false;
}
try {
if (validator != null) {
validator.run();
}
} catch (Exception e) {
sendError(sessionRef, cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, e.getMessage());
return false;
}
return true;
}
private void sendWsMsg(WebSocketSessionRef sessionRef, EntityDataUpdate update) {
sendWsMsg(sessionRef, update.getCmdId(), update);
private void sendUpdate(WebSocketSessionRef sessionRef, EntityDataUpdate update) {
sendUpdate(sessionRef, update.getCmdId(), update);
}
private void sendWsMsg(WebSocketSessionRef sessionRef, TelemetrySubscriptionUpdate update) {
sendWsMsg(sessionRef, update.getSubscriptionId(), update);
private void sendUpdate(WebSocketSessionRef sessionRef, TelemetrySubscriptionUpdate 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 {
String msg = JacksonUtil.OBJECT_MAPPER.writeValueAsString(update);
executor.submit(() -> {
@ -1055,47 +963,19 @@ public class DefaultWebSocketService implements WebSocketService {
.map(TenantProfile::getDefaultProfileConfiguration).orElse(null);
}
public static <W, C> WsCmdHandler<W, C> newCmdHandler(java.util.function.Function<W, C> cmdExtractor,
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);
public static <C extends WsCmd> WsCmdHandler<C> newCmdHandler(BiConsumer<WebSocketSessionRef, C> handler) {
return new WsCmdHandler<>(handler);
}
@RequiredArgsConstructor
public static class WsCmdHandler<W, C> {
private final java.util.function.Function<W, C> cmdExtractor;
private final BiConsumer<WebSocketSessionRef, C> handler;
public C extractCmd(W cmdsWrapper) {
return cmdExtractor.apply(cmdsWrapper);
}
@Getter
@SuppressWarnings("unchecked")
public static class WsCmdHandler<C extends WsCmd> {
protected final BiConsumer<WebSocketSessionRef, C> handler;
@SuppressWarnings("unchecked")
public void handle(WebSocketSessionRef sessionRef, Object cmd) {
public void handle(WebSocketSessionRef sessionRef, WsCmd 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;
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.sub.TelemetrySubscriptionUpdate;
@ -24,13 +25,15 @@ import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpda
*/
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);
}

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

@ -16,8 +16,7 @@
package org.thingsboard.server.service.ws;
import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Data;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.net.InetSocketAddress;
@ -27,15 +26,14 @@ import java.util.concurrent.atomic.AtomicInteger;
/**
* Created by ashvayka on 27.03.18.
*/
@RequiredArgsConstructor
@Builder
@Getter
@Data
public class WebSocketSessionRef {
private static final long serialVersionUID = 1L;
private final String sessionId;
private final SecurityUser securityCtx;
private SecurityUser securityCtx;
private final InetSocketAddress localAddress;
private final InetSocketAddress remoteAddress;
private final WebSocketSessionType sessionType;
@ -56,11 +54,13 @@ public class WebSocketSessionRef {
@Override
public String toString() {
return "WebSocketSessionRef{" +
"sessionId='" + sessionId + '\'' +
", localAddress=" + localAddress +
", remoteAddress=" + remoteAddress +
", sessionType=" + sessionType +
'}';
String info = "";
if (securityCtx != null) {
info += "[" + securityCtx.getTenantId() + "]";
info += "[" + securityCtx.getId() + "]";
}
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;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import java.util.Arrays;
import java.util.Optional;
@RequiredArgsConstructor
@Getter
@NoArgsConstructor
@AllArgsConstructor
public enum WebSocketSessionType {
TELEMETRY("telemetry"),
NOTIFICATIONS("notifications");
GENERAL(),
TELEMETRY("telemetry"), // deprecated
NOTIFICATIONS("notifications"); // deprecated
private final String name;
private String name;
public static Optional<WebSocketSessionType> forName(String name) {
return Arrays.stream(values())
.filter(sessionType -> sessionType.getName().equals(name))
.filter(sessionType -> StringUtils.equals(sessionType.name, name))
.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
* 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 {
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) {
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.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.service.ws.WsCmd;
import org.thingsboard.server.service.ws.WsCmdType;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MarkAllNotificationsAsReadCmd implements WsCmd {
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.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.service.ws.WsCmd;
import org.thingsboard.server.service.ws.WsCmdType;
import java.util.List;
import java.util.UUID;
@ -28,4 +30,9 @@ import java.util.UUID;
public class MarkNotificationsAsReadCmd implements WsCmd {
private int cmdId;
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;
import com.fasterxml.jackson.annotation.JsonIgnore;
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
@Deprecated
public class NotificationCmdsWrapper {
private NotificationsCountSubCmd unreadCountSubCmd;
@ -30,4 +40,13 @@ public class NotificationCmdsWrapper {
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.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.service.ws.WsCmd;
import org.thingsboard.server.service.ws.WsCmdType;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class NotificationsCountSubCmd implements WsCmd {
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.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.service.ws.WsCmd;
import org.thingsboard.server.service.ws.WsCmdType;
@Data
@NoArgsConstructor
@ -25,4 +27,9 @@ import lombok.NoArgsConstructor;
public class NotificationsSubCmd implements WsCmd {
private int cmdId;
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.Data;
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;
@Data
@ -25,4 +27,9 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd;
@AllArgsConstructor
public class NotificationsUnsubCmd implements UnsubscribeCmd, WsCmd {
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 {
private final int totalUnreadCount;
private final int sequenceNumber;
@Builder
@JsonCreator
public UnreadNotificationsCountUpdate(@JsonProperty("cmdId") int cmdId, @JsonProperty("errorCode") int errorCode,
@JsonProperty("errorMsg") String errorMsg,
@JsonProperty("totalUnreadCount") int totalUnreadCount) {
@JsonProperty("totalUnreadCount") int totalUnreadCount,
@JsonProperty("sequenceNumber") int sequenceNumber) {
super(cmdId, errorCode, errorMsg);
this.totalUnreadCount = totalUnreadCount;
this.sequenceNumber = sequenceNumber;
}
@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 Notification update;
private final int totalUnreadCount;
private final int sequenceNumber;
@Builder
@JsonCreator
@ -40,11 +41,13 @@ public class UnreadNotificationsUpdate extends CmdUpdate {
@JsonProperty("errorMsg") String errorMsg,
@JsonProperty("notifications") Collection<Notification> notifications,
@JsonProperty("update") Notification update,
@JsonProperty("totalUnreadCount") int totalUnreadCount) {
@JsonProperty("totalUnreadCount") int totalUnreadCount,
@JsonProperty("sequenceNumber") int sequenceNumber) {
super(cmdId, errorCode, errorMsg);
this.notifications = notifications;
this.update = update;
this.totalUnreadCount = totalUnreadCount;
this.sequenceNumber = sequenceNumber;
}
@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()
.cmdId(getSubscriptionId())
.totalUnreadCount(unreadCounter.get())
.sequenceNumber(sequence.incrementAndGet())
.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())
.notifications(getSortedNotifications())
.totalUnreadCount(totalUnreadCounter.get())
.sequenceNumber(sequence.incrementAndGet())
.build();
}
@ -68,6 +69,7 @@ public class NotificationsSubscription extends TbSubscription<NotificationsSubsc
.cmdId(getSubscriptionId())
.update(notification)
.totalUnreadCount(totalUnreadCounter.get())
.sequenceNumber(sequence.incrementAndGet())
.build();
}
@ -75,6 +77,7 @@ public class NotificationsSubscription extends TbSubscription<NotificationsSubsc
return UnreadNotificationsUpdate.builder()
.cmdId(getSubscriptionId())
.totalUnreadCount(totalUnreadCounter.get())
.sequenceNumber(sequence.incrementAndGet())
.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;
import com.fasterxml.jackson.annotation.JsonIgnore;
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.GetHistoryCmd;
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.EntityDataUnsubscribeCmd;
import java.util.Collection;
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
public class TelemetryPluginCmdsWrapper {
@Deprecated
public class TelemetryCmdsWrapper {
private List<AttributesSubscriptionCmd> attrSubCmds;
@ -58,4 +65,17 @@ public class TelemetryPluginCmdsWrapper {
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;
import lombok.NoArgsConstructor;
import org.thingsboard.server.service.ws.telemetry.TelemetryFeature;
import org.thingsboard.server.service.ws.WsCmdType;
/**
* @author Andrew Shvayka
@ -25,8 +25,8 @@ import org.thingsboard.server.service.ws.telemetry.TelemetryFeature;
public class AttributesSubscriptionCmd extends SubscriptionCmd {
@Override
public TelemetryFeature getType() {
return TelemetryFeature.ATTRIBUTES;
public WsCmdType getType() {
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.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.service.ws.WsCmdType;
/**
* @author Andrew Shvayka
@ -37,4 +38,8 @@ public class GetHistoryCmd implements TelemetryPluginCmd {
private int limit;
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 boolean unsubscribe;
public abstract TelemetryFeature getType();
@Override
public String toString() {
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;
import org.thingsboard.server.service.ws.WsCmd;
/**
* @author Andrew Shvayka
*/
public interface TelemetryPluginCmd {
public interface TelemetryPluginCmd extends WsCmd {
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.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.service.ws.telemetry.TelemetryFeature;
import org.thingsboard.server.service.ws.WsCmdType;
/**
* @author Andrew Shvayka
@ -26,6 +27,7 @@ import org.thingsboard.server.service.ws.telemetry.TelemetryFeature;
@NoArgsConstructor
@AllArgsConstructor
@Data
@EqualsAndHashCode(callSuper = true)
public class TimeseriesSubscriptionCmd extends SubscriptionCmd {
private long startTs;
@ -35,7 +37,7 @@ public class TimeseriesSubscriptionCmd extends SubscriptionCmd {
private String agg;
@Override
public TelemetryFeature getType() {
return TelemetryFeature.TIMESERIES;
public WsCmdType getType() {
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 lombok.Getter;
import org.thingsboard.server.common.data.query.AlarmCountQuery;
import org.thingsboard.server.service.ws.WsCmdType;
public class AlarmCountCmd extends DataCmd {
@ -31,4 +32,9 @@ public class AlarmCountCmd extends DataCmd {
super(cmdId);
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;
import lombok.Data;
import org.thingsboard.server.service.ws.WsCmdType;
@Data
public class AlarmCountUnsubscribeCmd implements UnsubscribeCmd {
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 lombok.Getter;
import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.service.ws.WsCmdType;
public class AlarmDataCmd extends DataCmd {
@ -30,4 +31,9 @@ public class AlarmDataCmd extends DataCmd {
super(cmdId);
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;
import lombok.Data;
import org.thingsboard.server.service.ws.WsCmdType;
@Data
public class AlarmDataUnsubscribeCmd implements UnsubscribeCmd {
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.Getter;
import org.thingsboard.server.service.ws.WsCmd;
@Data
public class DataCmd {
public abstract class DataCmd implements WsCmd {
@Getter
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 lombok.Getter;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.service.ws.WsCmdType;
public class EntityCountCmd extends DataCmd {
@ -31,4 +32,9 @@ public class EntityCountCmd extends DataCmd {
super(cmdId);
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;
import lombok.Data;
import org.thingsboard.server.service.ws.WsCmdType;
@Data
public class EntityCountUnsubscribeCmd implements UnsubscribeCmd {
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 lombok.Getter;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.service.ws.WsCmdType;
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;
}
@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;
import lombok.Data;
import org.thingsboard.server.service.ws.WsCmdType;
@Data
public class EntityDataUnsubscribeCmd implements UnsubscribeCmd {
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;
public interface UnsubscribeCmd {
import org.thingsboard.server.service.ws.WsCmd;
public interface UnsubscribeCmd extends WsCmd {
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}"
# 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}"
# Maximum time between WS session opening and sending auth command
auth_timeout_ms: "${TB_SERVER_WS_AUTH_TIMEOUT_MS:10000}"
rest:
server_side_rpc:
# 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:
# 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:

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

@ -52,8 +52,8 @@ public abstract class AbstractControllerTest extends AbstractNotifyEntityTest {
@LocalServerPort
protected int wsPort;
private volatile TbTestWebSocketClient wsClient; // lazy
private volatile TbTestWebSocketClient anotherWsClient; // lazy
protected volatile TbTestWebSocketClient wsClient; // lazy
protected volatile TbTestWebSocketClient anotherWsClient; // lazy
public TbTestWebSocketClient getWsClient() {
if (wsClient == null) {
@ -101,8 +101,15 @@ public abstract class AbstractControllerTest extends AbstractNotifyEntityTest {
}
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();
if (!path.contains("token=")) {
wsClient.authenticate(token);
}
return wsClient;
}

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

@ -15,19 +15,41 @@
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import org.junit.After;
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.model.SecuritySettings;
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 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.status;
@DaoSqlTest
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
public void testGetUser() throws Exception {
@ -84,4 +106,65 @@ public class AuthControllerTest extends AbstractControllerTest {
.andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name())))
.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.Before;
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.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
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.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.dao.device.DeviceDao;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.nio.file.Files;
@ -68,7 +62,6 @@ import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.WINDOWS;
@TestPropertySource(properties = {
"device.connectivity.mqtts.pem_cert_file=/tmp/" + CA_ROOT_CERT_PEM
})
@ContextConfiguration(classes = {DeviceConnectivityControllerTest.Config.class})
@DaoSqlTest
public class DeviceConnectivityControllerTest extends AbstractControllerTest {
@ -102,14 +95,6 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
private DeviceProfileId mqttDeviceProfileId;
private DeviceProfileId coapDeviceProfileId;
static class Config {
@Bean
@Primary
public DeviceDao deviceDao(DeviceDao deviceDao) {
return Mockito.mock(DeviceDao.class, AdditionalAnswers.delegatesTo(deviceDao));
}
}
@Before
public void beforeTest() throws Exception {
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 " +
"-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(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 " +
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_CERT_PEM + " -h localhost -p 8883 " +
"-t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId()));
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}\"",
credentials.getCredentialsId()));
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 && " +
"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}\"\"",
"/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_CERT_PEM + " -h host.docker.internal -p 8883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"\"",
credentials.getCredentialsId()));
JsonNode linuxCoapCommands = commands.get(COAP);
@ -278,8 +263,8 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
JsonNode mqttCommands = commands.get(MQTT);
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()));
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(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 " +
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_CERT_PEM + " -h localhost -p 8883 " +
"-t %s -u \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId()));
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}\"",
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 " +
"/bin/sh -c \"curl -f -S -o ca-root.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}\"\"",
"/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_CERT_PEM + " -h host.docker.internal -p 8883 -t %s -u \"%s\" -m \"{temperature:25}\"\"",
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<>() {
});
String expectedContainerName = deviceName.replaceAll("[^A-Za-z0-9_.-]", "");;
String expectedContainerName = deviceName.replaceAll("[^A-Za-z0-9_.-]", "");
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()));
@ -349,16 +334,16 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
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 " +
"-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(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile ca-root.pem -h ::1 -p 8883 " +
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_CERT_PEM + " -h ::1 -p 8883 " +
"-t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId()));
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" +
" -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 " +
"/bin/sh -c \"curl -f -S -o ca-root.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}\"\"",
"/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_CERT_PEM + " -h host.docker.internal -p 8883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"\"",
credentials.getCredentialsId()));
JsonNode linuxCoapCommands = commands.get(COAP);
@ -403,16 +388,16 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
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 " +
"-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(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(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_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()));
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" +
" -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 " +
"/bin/sh -c \"curl -f -S -o ca-root.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}\"\"",
"/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_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()));
JsonNode linuxCoapCommands = commands.get(COAP);
@ -459,8 +444,8 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
JsonNode mqttCommands = commands.get(MQTT);
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));
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(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 " +
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_CERT_PEM + " -h localhost -p 8883 " +
"-t %s -i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password));
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}\"",
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 " +
"/bin/sh -c \"curl -f -S -o ca-root.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}\"\"",
"/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_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));
}
@ -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 " +
"-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(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile ca-root.pem -h test.domain -p 8883 " +
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_CERT_PEM + " -h test.domain -p 8883 " +
"-t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId()));
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}\"",
credentials.getCredentialsId()));
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 && " +
"mosquitto_pub -d -q 1 --cafile ca-root.pem -h test.domain -p 8883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"\"",
"/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_CERT_PEM + " -h test.domain -p 8883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"\"",
credentials.getCredentialsId()));
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;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
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.EntityFilter;
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.v2.AlarmCountCmd;
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.EntityDataCmd;
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
public void onMessage(String s) {
log.info("RECEIVED: {}", s);
@ -105,24 +110,6 @@ public class TbTestWebSocketClient extends WebSocketClient {
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() {
return waitForUpdate(false);
}
@ -240,11 +227,7 @@ public class TbTestWebSocketClient extends WebSocketClient {
cmd.setEntityId(entityId.getId().toString());
cmd.setScope(scope);
cmd.setKeys(String.join(",", keys));
TelemetryPluginCmdsWrapper cmdsWrapper = new TelemetryPluginCmdsWrapper();
cmdsWrapper.setAttrSubCmds(List.of(cmd));
JsonNode msg = JacksonUtil.valueToTree(cmdsWrapper);
((ObjectNode) msg.get("attrSubCmds").get(0)).remove("type");
send(msg.toString());
send(cmd);
return JacksonUtil.toJsonNode(waitForReply());
}
@ -288,4 +271,10 @@ public class TbTestWebSocketClient extends WebSocketClient {
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
public void testEntityCountCmd_filterTypeSingularCompatibilityTest() {
public void testEntityCountCmd_filterTypeSingularCompatibilityTest() throws Exception {
ObjectNode oldFormatDeviceTypeFilterSingular = JacksonUtil.newObjectNode();
oldFormatDeviceTypeFilterSingular.put("type", "deviceType");
oldFormatDeviceTypeFilterSingular.put("deviceType", "default");
@ -667,9 +667,10 @@ public class WebsocketApiTest extends AbstractControllerTest {
ObjectNode wrapperNode = JacksonUtil.newObjectNode();
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.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 java.io.IOException;
import java.util.Collection;
import java.util.Deque;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
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.willDoNothing;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
@ -79,7 +83,9 @@ class TbWebSocketHandlerTest {
asyncRemote = mock(RemoteEndpoint.Async.class);
willReturn(asyncRemote).given(nativeSession).getAsyncRemote();
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
@ -157,4 +163,25 @@ class TbWebSocketHandlerTest {
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
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();
wsClient.authenticate(token);
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.service.ws.notification.cmd.MarkAllNotificationsAsReadCmd;
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.NotificationsSubCmd;
import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsCountUpdate;
@ -49,40 +48,27 @@ public class NotificationApiWsClient extends TbTestWebSocketClient {
private int unreadCount;
private List<Notification> notifications;
public NotificationApiWsClient(String wsUrl, String token) throws URISyntaxException {
super(new URI(wsUrl + "/api/ws/plugins/notifications?token=" + token));
public NotificationApiWsClient(String wsUrl) throws URISyntaxException {
super(new URI(wsUrl + "/api/ws"));
}
public NotificationApiWsClient subscribeForUnreadNotifications(int limit) {
NotificationCmdsWrapper cmdsWrapper = new NotificationCmdsWrapper();
cmdsWrapper.setUnreadSubCmd(new NotificationsSubCmd(1, limit));
sendCmd(cmdsWrapper);
send(new NotificationsSubCmd(1, limit));
this.limit = limit;
return this;
}
public NotificationApiWsClient subscribeForUnreadNotificationsCount() {
NotificationCmdsWrapper cmdsWrapper = new NotificationCmdsWrapper();
cmdsWrapper.setUnreadCountSubCmd(new NotificationsCountSubCmd(2));
sendCmd(cmdsWrapper);
send(new NotificationsCountSubCmd(2));
return this;
}
public void markNotificationAsRead(UUID... notifications) {
NotificationCmdsWrapper cmdsWrapper = new NotificationCmdsWrapper();
cmdsWrapper.setMarkAsReadCmd(new MarkNotificationsAsReadCmd(newCmdId(), Arrays.asList(notifications)));
sendCmd(cmdsWrapper);
send(new MarkNotificationsAsReadCmd(newCmdId(), Arrays.asList(notifications)));
}
public void markAllNotificationsAsRead() {
NotificationCmdsWrapper cmdsWrapper = new NotificationCmdsWrapper();
cmdsWrapper.setMarkAllAsReadCmd(new MarkAllNotificationsAsReadCmd(newCmdId()));
sendCmd(cmdsWrapper);
}
public void sendCmd(NotificationCmdsWrapper cmdsWrapper) {
String cmd = JacksonUtil.toString(cmdsWrapper);
send(cmd);
send(new MarkAllNotificationsAsReadCmd(newCmdId()));
}
@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);
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.getEmail()).isEqualTo(securityUser.getEmail());
assertThat(parsedSecurityUser.getUserPrincipal()).matches(userPrincipal -> {
@ -135,7 +135,7 @@ public class JwtTokenFactoryTest {
JwtToken refreshToken = tokenFactory.createRefreshToken(securityUser);
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.getUserPrincipal()).matches(userPrincipal -> {
return userPrincipal.getType().equals(securityUser.getUserPrincipal().getType())
@ -159,7 +159,7 @@ public class JwtTokenFactoryTest {
JwtToken preVerificationToken = tokenFactory.createPreVerificationToken(securityUser, 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.getAuthority()).isEqualTo(Authority.PRE_VERIFICATION_TOKEN);
assertThat(parsedSecurityUser.getTenantId()).isEqualTo(securityUser.getTenantId());
@ -198,7 +198,7 @@ public class JwtTokenFactoryTest {
}
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 -> {
Calendar expirationTime = Calendar.getInstance();
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
SECONDS.sleep(1);
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId()));
assertTrue(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId()));
assertTrue(tokenOutdatingService.isOutdated(jwtToken.getToken(), securityUser.getId()));
SECONDS.sleep(1);
JwtToken newJwtToken = tokenFactory.createAccessJwtToken(securityUser);
assertFalse(tokenOutdatingService.isOutdated(newJwtToken, securityUser.getId()));
assertFalse(tokenOutdatingService.isOutdated(newJwtToken.getToken(), securityUser.getId()));
}
@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.DeviceCredentialsType;
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.EntityDataUpdate;
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());
EntityDataCmd cmd = new EntityDataCmd(1, edq, null, latestCmd, null);
TelemetryPluginCmdsWrapper wrapper = new TelemetryPluginCmdsWrapper();
wrapper.setEntityDataCmds(Collections.singletonList(cmd));
getWsClient().send(JacksonUtil.toString(wrapper));
getWsClient().send(cmd);
getWsClient().waitForReply();
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
@Length(fieldName = "title")
private String title;
@Length(fieldName = "image", max = 1000000)
private String image;
@Valid
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
@ApiModelProperty(position = 11, value = "Device Profile 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. ")
private String image;
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
@ApiModelProperty(position = 11, value = "Asset Profile 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. ")
private String image;
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),
TOO_MANY_REQUESTS(33),
TOO_MANY_UPDATES(34),
SUBSCRIPTION_VIOLATION(40);
SUBSCRIPTION_VIOLATION(40),
PASSWORD_VIOLATION(45);
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." )
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." )
private Integer minimumUppercaseLetters;
@ApiModelProperty(position = 1, value = "Minimum number of lowercase letters in the password." )
@ -37,6 +39,8 @@ public class UserPasswordPolicy implements Serializable {
private Integer minimumSpecialCharacters;
@ApiModelProperty(position = 1, value = "Allow whitespaces")
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." )
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" })
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.")
private String image;
@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)
private String title;
@Length(fieldName = "image", max = 1000000)
@Getter
@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)
@ -66,7 +65,6 @@ public class WidgetsBundle extends BaseData<WidgetsBundleId> implements HasName,
@NoXss
@Length(fieldName = "description", max = 1024)
@Getter
@Setter
@ApiModelProperty(position = 7, value = "Description", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
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.tuple.Pair;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Base64Utils;
import org.thingsboard.common.util.JacksonUtil;
@ -274,7 +273,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic
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
public boolean replaceBase64WithImageUrl(HasImage entity, String type) {
log.trace("Executing replaceBase64WithImageUrl [{}] [{}] [{}]", entity.getTenantId(), type, entity.getName());
@ -289,7 +288,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic
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
public boolean replaceBase64WithImageUrl(WidgetTypeDetails entity) {
log.trace("Executing replaceBase64WithImageUrl [{}] [WidgetTypeDetails] [{}]", entity.getTenantId(), entity.getId());
@ -315,7 +314,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic
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
public boolean replaceBase64WithImageUrl(Dashboard entity) {
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);
}
} 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());
}

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 lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.thingsboard.server.common.data.BaseData;
@ -129,7 +130,7 @@ public abstract class DataValidator<D extends BaseData<?>> {
EntityType entityType) {
if (maxSumDataSize > 0) {
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;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.TbResource;
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.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.widget.BaseWidgetType;
@ -83,19 +85,8 @@ public class ResourceDataValidator extends DataValidator<TbResource> {
if (resource.getResourceType() == null) {
throw new DataValidationException("Resource type should be specified!");
}
if (!resource.getTenantId().isSysTenantId() && resource.getData() != null) {
DefaultTenantProfileConfiguration profileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration();
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 (resource.getData() != null) {
validateResourceSize(resource.getTenantId(), resource.getId(), resource.getData().length);
}
if (StringUtils.isEmpty(resource.getFileName())) {
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
public void validateDelete(TenantId tenantId, EntityId resourceId) {
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) {
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) {
log.info("[{}] Queue polling was interrupted", logName);
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 {
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);
@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,
@Param("resourceType") String resourceType,
@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.edge.EdgeEventService;
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.nosql.CassandraStatementTask;
import org.thingsboard.server.dao.nosql.TbResultSetFuture;
@ -393,4 +394,6 @@ public interface TbContext {
WidgetTypeService getWidgetTypeService();
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 {
TelemetryPluginCmdsWrapper,
TelemetrySubscriber,
WebsocketDataMsg
} from '@shared/models/telemetry/telemetry.models';
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AuthService } from '@core/auth/auth.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';
// @dynamic
@Injectable({
providedIn: 'root'
})
export class NotificationWebsocketService extends WebsocketService<NotificationSubscriber> {
export class NotificationWebsocketService extends WebsocketService<TelemetrySubscriber> {
cmdWrapper: NotificationPluginCmdWrapper;
constructor(protected store: Store<AppState>,
constructor(private telemetryWebsocketService: TelemetryWebsocketService,
protected store: Store<AppState>,
protected authService: AuthService,
protected ngZone: NgZone,
@Inject(WINDOW) protected window: Window) {
super(store, authService, ngZone, 'api/ws/plugins/notifications', new NotificationPluginCmdWrapper(), window);
this.errorName = 'WebSocket Notification Error';
super(store, authService, ngZone, 'api/ws/plugins/telemetry', new TelemetryPluginCmdsWrapper(), window);
}
public subscribe(subscriber: NotificationSubscriber) {
this.isActive = true;
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 subscribe(subscriber: TelemetrySubscriber) {
this.telemetryWebsocketService.subscribe(subscriber);
}
public update(subscriber: NotificationSubscriber) {
if (!this.isReconnect) {
subscriber.subscriptionCommands.forEach(
(subscriptionCommand) => {
if (subscriptionCommand.cmdId && subscriptionCommand instanceof UnreadSubCmd) {
this.cmdWrapper.unreadSubCmd = subscriptionCommand;
}
}
);
this.publishCommands();
}
public update(subscriber: TelemetrySubscriber) {
this.telemetryWebsocketService.update(subscriber);
}
public unsubscribe(subscriber: NotificationSubscriber) {
if (this.isActive) {
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();
}
public unsubscribe(subscriber: TelemetrySubscriber) {
this.telemetryWebsocketService.unsubscribe(subscriber);
}
processOnMessage(message: WebsocketNotificationMsg) {
let subscriber: NotificationSubscriber;
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));
}
}
processOnMessage(message: WebsocketDataMsg) {
this.telemetryWebsocketService.processOnMessage(message);
}
}

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

@ -16,28 +16,36 @@
import { Inject, Injectable, NgZone } from '@angular/core';
import {
AlarmCountCmd, AlarmCountUnsubscribeCmd,
AlarmCountCmd,
AlarmCountUnsubscribeCmd,
AlarmCountUpdate,
AlarmDataCmd,
AlarmDataUnsubscribeCmd,
AlarmDataUpdate,
AttributesSubscriptionCmd,
EntityCountCmd,
EntityCountUnsubscribeCmd,
EntityCountUpdate,
EntityDataCmd,
EntityDataUnsubscribeCmd,
EntityDataUpdate,
GetHistoryCmd, isAlarmCountUpdateMsg,
isAlarmCountUpdateMsg,
isAlarmDataUpdateMsg,
isEntityCountUpdateMsg,
isEntityDataUpdateMsg,
isNotificationCountUpdateMsg,
isNotificationsUpdateMsg,
MarkAllAsReadCmd,
MarkAsReadCmd,
NotificationCountUpdate,
NotificationSubscriber,
NotificationsUpdate,
SubscriptionCmd,
SubscriptionUpdate,
TelemetryFeature,
TelemetryPluginCmdsWrapper,
TelemetrySubscriber,
TimeseriesSubscriptionCmd,
UnreadCountSubCmd,
UnreadSubCmd,
UnsubscribeCmd,
WebsocketDataMsg
} from '@app/shared/models/telemetry/telemetry.models';
import { Store } from '@ngrx/store';
@ -58,7 +66,7 @@ export class TelemetryWebsocketService extends WebsocketService<TelemetrySubscri
protected authService: AuthService,
protected ngZone: NgZone,
@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) {
@ -66,25 +74,11 @@ export class TelemetryWebsocketService extends WebsocketService<TelemetrySubscri
subscriber.subscriptionCommands.forEach(
(subscriptionCommand) => {
const cmdId = this.nextCmdId();
this.subscribersMap.set(cmdId, subscriber);
subscriptionCommand.cmdId = cmdId;
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);
if (!(subscriptionCommand instanceof MarkAsReadCmd) && !(subscriptionCommand instanceof MarkAllAsReadCmd)) {
this.subscribersMap.set(cmdId, subscriber);
}
subscriptionCommand.cmdId = cmdId;
this.cmdWrapper.cmds.push(subscriptionCommand);
}
);
this.subscribersCount++;
@ -95,8 +89,8 @@ export class TelemetryWebsocketService extends WebsocketService<TelemetrySubscri
if (!this.isReconnect) {
subscriber.subscriptionCommands.forEach(
(subscriptionCommand) => {
if (subscriptionCommand.cmdId && subscriptionCommand instanceof EntityDataCmd) {
this.cmdWrapper.entityDataCmds.push(subscriptionCommand);
if (subscriptionCommand.cmdId && (subscriptionCommand instanceof EntityDataCmd || subscriptionCommand instanceof UnreadSubCmd)) {
this.cmdWrapper.cmds.push(subscriptionCommand);
}
}
);
@ -110,27 +104,27 @@ export class TelemetryWebsocketService extends WebsocketService<TelemetrySubscri
(subscriptionCommand) => {
if (subscriptionCommand instanceof SubscriptionCmd) {
subscriptionCommand.unsubscribe = true;
if (subscriptionCommand.getType() === TelemetryFeature.TIMESERIES) {
this.cmdWrapper.tsSubCmds.push(subscriptionCommand as TimeseriesSubscriptionCmd);
} else {
this.cmdWrapper.attrSubCmds.push(subscriptionCommand as AttributesSubscriptionCmd);
}
this.cmdWrapper.cmds.push(subscriptionCommand);
} else if (subscriptionCommand instanceof EntityDataCmd) {
const entityDataUnsubscribeCmd = new EntityDataUnsubscribeCmd();
entityDataUnsubscribeCmd.cmdId = subscriptionCommand.cmdId;
this.cmdWrapper.entityDataUnsubscribeCmds.push(entityDataUnsubscribeCmd);
this.cmdWrapper.cmds.push(entityDataUnsubscribeCmd);
} else if (subscriptionCommand instanceof AlarmDataCmd) {
const alarmDataUnsubscribeCmd = new AlarmDataUnsubscribeCmd();
alarmDataUnsubscribeCmd.cmdId = subscriptionCommand.cmdId;
this.cmdWrapper.alarmDataUnsubscribeCmds.push(alarmDataUnsubscribeCmd);
this.cmdWrapper.cmds.push(alarmDataUnsubscribeCmd);
} else if (subscriptionCommand instanceof EntityCountCmd) {
const entityCountUnsubscribeCmd = new EntityCountUnsubscribeCmd();
entityCountUnsubscribeCmd.cmdId = subscriptionCommand.cmdId;
this.cmdWrapper.entityCountUnsubscribeCmds.push(entityCountUnsubscribeCmd);
this.cmdWrapper.cmds.push(entityCountUnsubscribeCmd);
} else if (subscriptionCommand instanceof AlarmCountCmd) {
const alarmCountUnsubscribeCmd = new AlarmCountUnsubscribeCmd();
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;
if (cmdId) {
@ -145,29 +139,28 @@ export class TelemetryWebsocketService extends WebsocketService<TelemetrySubscri
}
processOnMessage(message: WebsocketDataMsg) {
let subscriber: TelemetrySubscriber;
if (isEntityDataUpdateMsg(message)) {
subscriber = this.subscribersMap.get(message.cmdId);
if (subscriber) {
subscriber.onEntityData(new EntityDataUpdate(message));
}
} else if (isAlarmDataUpdateMsg(message)) {
let subscriber: TelemetrySubscriber | NotificationSubscriber;
if ('cmdId' in message && message.cmdId) {
subscriber = this.subscribersMap.get(message.cmdId);
if (subscriber) {
subscriber.onAlarmData(new AlarmDataUpdate(message));
}
} else if (isEntityCountUpdateMsg(message)) {
subscriber = this.subscribersMap.get(message.cmdId);
if (subscriber) {
subscriber.onEntityCount(new EntityCountUpdate(message));
}
} else if (isAlarmCountUpdateMsg(message)) {
subscriber = this.subscribersMap.get(message.cmdId);
if (subscriber) {
subscriber.onAlarmCount(new AlarmCountUpdate(message));
if (subscriber instanceof NotificationSubscriber) {
if (isNotificationCountUpdateMsg(message)) {
subscriber.onNotificationCountUpdate(new NotificationCountUpdate(message));
} else if (isNotificationsUpdateMsg(message)) {
subscriber.onNotificationsUpdate(new NotificationsUpdate(message));
}
} else if (subscriber instanceof TelemetrySubscriber) {
if (isEntityDataUpdateMsg(message)) {
subscriber.onEntityData(new EntityDataUpdate(message));
} else if (isAlarmDataUpdateMsg(message)) {
subscriber.onAlarmData(new AlarmDataUpdate(message));
} else if (isEntityCountUpdateMsg(message)) {
subscriber.onEntityCount(new EntityCountUpdate(message));
} else if (isAlarmCountUpdateMsg(message)) {
subscriber.onAlarmCount(new AlarmCountUpdate(message));
}
}
} else if (message.subscriptionId) {
subscriber = this.subscribersMap.get(message.subscriptionId);
} else if ('subscriptionId' in message && message.subscriptionId) {
subscriber = this.subscribersMap.get(message.subscriptionId) as TelemetrySubscriber;
if (subscriber) {
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 { selectIsAuthenticated } from '@core/auth/auth.selectors';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
import { WebsocketNotificationMsg } from '@shared/models/websocket/notification-ws.models';
import { CmdUpdateMsg } from '@shared/models/telemetry/telemetry.models';
import {
AuthWsCmd,
CmdUpdateMsg,
NotificationSubscriber,
TelemetrySubscriber,
WebsocketDataMsg
} from '@shared/models/telemetry/telemetry.models';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import Timeout = NodeJS.Timeout;
@ -42,13 +47,13 @@ export abstract class WebsocketService<T extends WsSubscriber> implements WsServ
lastCmdId = 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';
@ -69,23 +74,23 @@ export abstract class WebsocketService<T extends WsSubscriber> implements WsServ
if (!port) {
port = '443';
}
this.notificationUri = 'wss:';
this.wsUri = 'wss:';
} else {
if (!port) {
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 unsubscribe(subscriber: T);
abstract processOnMessage(message: any);
abstract processOnMessage(message: WebsocketDataMsg);
protected nextCmdId(): number {
this.lastCmdId++;
@ -158,13 +163,13 @@ export abstract class WebsocketService<T extends WsSubscriber> implements WsServ
}
private openSocket(token: string) {
const uri = `${this.notificationUri}?token=${token}`;
this.dataStream = webSocket(
const uri = `${this.wsUri}`;
this.dataStream = webSocket<CmdUpdateMsg>(
{
url: uri,
openObserver: {
next: () => {
this.onOpen();
this.onOpen(token);
}
},
closeObserver: {
@ -176,9 +181,9 @@ export abstract class WebsocketService<T extends WsSubscriber> implements WsServ
);
this.dataStream.subscribe({
next: (message) => {
next: (message: CmdUpdateMsg) => {
this.ngZone.runOutsideAngular(() => {
this.onMessage(message as WebsocketNotificationMsg);
this.onMessage(message);
});
},
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.isOpened = true;
this.cmdWrapper.setAuth(token);
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
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) {
this.showWsError(message.errorCode, message.errorMsg);
} else {
this.processOnMessage(message);
this.processOnMessage(message as WebsocketDataMsg);
}
this.checkToClose();
}
@ -259,5 +265,4 @@ export abstract class WebsocketService<T extends WsSubscriber> implements WsServ
message, type: 'error'
}));
}
}

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

@ -25,11 +25,11 @@ import {
} from '@angular/core';
import { NotificationWebsocketService } from '@core/ws/notification-websocket.service';
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 { TbPopoverService } from '@shared/components/popover.service';
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 { selectIsAuthenticated } from '@core/auth/auth.selectors';
import { AppState } from '@core/core.state';
@ -77,11 +77,11 @@ export class NotificationBellComponent implements OnDestroy {
if ($event) {
$event.stopPropagation();
}
this.unsubscribeSubscription();
const trigger = createVersionButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
this.unsubscribeSubscription();
const showNotificationPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, ShowNotificationPopoverComponent, 'bottom', true, null,
{
@ -100,7 +100,9 @@ export class NotificationBellComponent implements OnDestroy {
private initSubscription() {
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();
}
@ -108,5 +110,6 @@ export class NotificationBellComponent implements OnDestroy {
private unsubscribeSubscription() {
this.notificationCountSubscriber.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 { NotificationWebsocketService } from '@core/ws/notification-websocket.service';
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 { NotificationSubscriber } from '@shared/models/websocket/notification-ws.models';
import { NotificationSubscriber } from '@shared/models/telemetry/telemetry.models';
@Component({
selector: 'tb-show-notification-popover',
@ -71,7 +71,9 @@ export class ShowNotificationPopoverComponent extends PageComponent implements O
}),
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();
}

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)'),
batteryLevelColor: {
color: 'rgba(92, 223, 144, 1)',
color: 'rgba(224, 224, 224, 1)',
type: ColorType.range,
rangeList: [
{from: 0, to: 25, color: 'rgba(227, 71, 71, 1)'},
@ -88,7 +88,7 @@ export const batteryLevelDefaultSettings: BatteryLevelWidgetSettings = {
colorFunction: defaultColorFunction
},
batteryShapeColor: {
color: 'rgba(92, 223, 144, 0.32)',
color: 'rgba(224, 224, 224, 0.32)',
type: ColorType.range,
rangeList: [
{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-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 class="tb-form-panel-title" translate>widgets.background.background</div>
<tb-toggle-select formControlName="type" fxFlex selectMediaBreakpoint="xs">
@ -28,7 +28,7 @@
</tb-toggle-select>
</div>
<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>
<tb-color-input asBoxInput
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;
color: rgba(0, 0, 0, 0.87);
}
.tb-background-form-panel {
height: 192px;
.tb-background-color-field {
height: auto;
}
}
.tb-background-settings-preview {
flex: 1;
background: rgba(0, 0, 0, 0.04);

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

Loading…
Cancel
Save