diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java index e88d812f5b..f7cc08391a 100644 --- a/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java @@ -64,6 +64,7 @@ import org.thingsboard.server.service.entitiy.dashboard.TbDashboardService; import org.thingsboard.server.service.entitiy.device.TbDeviceService; import org.thingsboard.server.service.entitiy.device.profile.TbDeviceProfileService; import org.thingsboard.server.service.entitiy.widgets.type.TbWidgetTypeService; +import org.thingsboard.server.service.install.ProjectInfo; import org.thingsboard.server.service.rule.TbRuleChainService; import org.thingsboard.server.service.security.model.SecurityUser; @@ -112,6 +113,7 @@ public class DefaultIotHubService implements IotHubService { private final DeviceService deviceService; private final TbDeviceService tbDeviceService; private final SolutionService solutionService; + private final ProjectInfo projectInfo; // Field names of the marketplace version JSON payload. Both the install path and the // install-plan resolver parse the same shape, so the contract lives here in one place. @@ -196,10 +198,11 @@ public class DefaultIotHubService implements IotHubService { } try { - iotHubRestClient.reportVersionInstalled(versionId); + InstallReport report = buildInstallReport(tenantId.getId(), user.getId().getId(), + projectInfo.getProjectVersion(), projectInfo.getProductType()); + iotHubRestClient.reportVersionInstalled(versionId, report); } catch (Exception e) { - // Counter ping is best-effort — do not fail the install if it errors. - log.warn("[{}] Failed to report install counter for version {}: {}", tenantId, versionId, e.getMessage()); + log.warn("[{}] Failed to report install for version {}: {}", tenantId, versionId, e.getMessage()); } log.info("[{}] Successfully installed IoT Hub item version: {} (type: {})", tenantId, itemName, itemType); return installedItem; @@ -611,6 +614,13 @@ public class DefaultIotHubService implements IotHubService { } } + static InstallReport buildInstallReport(UUID tenantId, UUID userId, String tbVersion, String edition) { + String salt = tenantId.toString(); + String tenantHash = sha256(salt + tenantId); + String userHash = sha256(salt + userId); + return new InstallReport(tenantHash, userHash, tbVersion, edition); + } + @Override public InstallItemVersionResult registerDeviceInstall(SecurityUser user, String versionId, DeviceInstalledItemDescriptor descriptor) { TenantId tenantId = user.getTenantId(); @@ -650,10 +660,11 @@ public class DefaultIotHubService implements IotHubService { } try { - iotHubRestClient.reportVersionInstalled(versionId); + InstallReport report = buildInstallReport(tenantId.getId(), user.getId().getId(), + projectInfo.getProjectVersion(), projectInfo.getProductType()); + iotHubRestClient.reportVersionInstalled(versionId, report); } catch (Exception e) { - // Counter ping is best-effort — do not fail the install if it errors. - log.warn("[{}] Failed to report install counter for version {}: {}", tenantId, versionId, e.getMessage()); + log.warn("[{}] Failed to report install for version {}: {}", tenantId, versionId, e.getMessage()); } log.info("[{}] Registered device package install: {} (version {})", tenantId, itemName, version); diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallReport.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallReport.java new file mode 100644 index 0000000000..3b253e426c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/InstallReport.java @@ -0,0 +1,19 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.iot_hub; + +public record InstallReport(String tenantHash, String userHash, String tbVersion, String edition) { +} diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java index c67fef55a8..ae3cf5b07e 100644 --- a/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/IotHubRestClient.java @@ -120,9 +120,9 @@ public class IotHubRestClient { }); } - public void reportVersionInstalled(String versionId) { + public void reportVersionInstalled(String versionId, InstallReport report) { String url = baseUrl + "/api/versions/" + versionId + "/install"; log.debug("Reporting IoT Hub version installed: {}", url); - restTemplate.postForObject(url, null, Void.class); + restTemplate.postForObject(url, report, Void.class); } } diff --git a/application/src/test/java/org/thingsboard/server/service/iot_hub/InstallReportTest.java b/application/src/test/java/org/thingsboard/server/service/iot_hub/InstallReportTest.java new file mode 100644 index 0000000000..73f968fc01 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/iot_hub/InstallReportTest.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.iot_hub; + +import org.junit.jupiter.api.Test; + +import java.security.MessageDigest; +import java.nio.charset.StandardCharsets; +import java.util.HexFormat; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class InstallReportTest { + + private static String sha256(String s) throws Exception { + MessageDigest d = MessageDigest.getInstance("SHA-256"); + return HexFormat.of().formatHex(d.digest(s.getBytes(StandardCharsets.UTF_8))); + } + + @Test + void buildInstallReport_hashesMatchSaltedFormula() throws Exception { + UUID tenantId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + + InstallReport r = DefaultIotHubService.buildInstallReport(tenantId, userId, "4.2.0", "CE"); + + assertEquals(sha256(tenantId.toString() + tenantId), r.tenantHash()); + assertEquals(sha256(tenantId.toString() + userId), r.userHash()); + assertEquals("4.2.0", r.tbVersion()); + assertEquals("CE", r.edition()); + } +}