From 8be7a23b15b6028a5e7071c0de1d242f02101839 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 25 Mar 2026 15:17:42 +0200 Subject: [PATCH 01/25] Added automatic SSL/TLS certificate reload for transports without service restart --- .../src/main/resources/thingsboard.yml | 9 + .../coapserver/DefaultCoapServerService.java | 131 +++++- .../server/coapserver/TbCoapDtlsSettings.java | 6 +- .../CoapDtlsCertificateReloadTest.java | 235 +++++++++++ .../coapserver/TbCoapDtlsSettingsTest.java | 8 +- .../server/common/data/ResourceUtils.java | 19 +- .../server/common/data/ResourceUtilsTest.java | 39 ++ .../transport/http/DeviceApiController.java | 4 - .../transport/http/HttpTransportContext.java | 4 +- .../LwM2MTransportBootstrapService.java | 43 +- .../config/LwM2MTransportBootstrapConfig.java | 29 ++ .../config/LwM2MTransportServerConfig.java | 32 ++ .../server/DefaultLwM2mTransportService.java | 65 ++- .../LwM2mBootstrapCertificateReloadTest.java | 192 +++++++++ .../LwM2mServerCertificateReloadTest.java | 188 +++++++++ .../mqtt/MqttSslHandlerProvider.java | 35 +- .../transport/mqtt/MqttTransportContext.java | 3 - .../mqtt/MqttSslHandlerProviderTest.java | 197 +++++++++ .../common/transport/DeviceDeletedEvent.java | 5 + .../common/transport/SessionMsgListener.java | 3 - .../common/transport/TransportContext.java | 5 - .../common/transport/TransportService.java | 4 +- .../transport/TransportServiceCallback.java | 3 - .../config/ssl/AbstractSslCredentials.java | 93 +++-- .../config/ssl/KeystoreSslCredentials.java | 16 + .../config/ssl/PemSslCredentials.java | 49 ++- .../transport/config/ssl/SslCredentials.java | 7 + .../config/ssl/SslCredentialsConfig.java | 27 ++ .../SslCredentialsWebServerCustomizer.java | 119 ++++-- .../service/CertificateReloadManager.java | 272 +++++++++++++ .../service/DefaultTransportService.java | 16 +- .../transport/service/SessionMetaData.java | 8 +- .../service/ToRuleEngineMsgEncoder.java | 3 - .../ToTransportMsgResponseDecoder.java | 3 - .../service/TransportApiRequestEncoder.java | 3 - .../service/TransportApiResponseDecoder.java | 3 - .../session/DeviceAwareSessionContext.java | 3 - .../transport/session/SessionContext.java | 1 + .../common/transport/util/JsonUtils.java | 16 +- .../server/common/transport/util/SslUtil.java | 7 +- .../config/ssl/SslCredentialsConfigTest.java | 182 +++++++++ ...SslCredentialsWebServerCustomizerTest.java | 277 +++++++++++++ .../service/CertificateReloadManagerTest.java | 383 ++++++++++++++++++ .../src/main/resources/tb-coap-transport.yml | 9 + .../src/main/resources/tb-http-transport.yml | 9 + .../src/main/resources/tb-lwm2m-transport.yml | 9 + .../src/main/resources/tb-mqtt-transport.yml | 9 + 47 files changed, 2586 insertions(+), 197 deletions(-) create mode 100644 common/coap-server/src/test/java/org/thingsboard/server/coapserver/CoapDtlsCertificateReloadTest.java create mode 100644 common/data/src/test/java/org/thingsboard/server/common/data/ResourceUtilsTest.java create mode 100644 common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2mBootstrapCertificateReloadTest.java create mode 100644 common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerCertificateReloadTest.java create mode 100644 common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProviderTest.java create mode 100644 common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java create mode 100644 common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsConfigTest.java create mode 100644 common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsWebServerCustomizerTest.java create mode 100644 common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 81b56aff28..419e6d3dfd 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1394,6 +1394,15 @@ transport: branch: "${TB_GATEWAY_DASHBOARD_SYNC_BRANCH:release/4.0.0}" # Fetch frequency in hours for gateways dashboard repository fetch_frequency: "${TB_GATEWAY_DASHBOARD_SYNC_FETCH_FREQUENCY:24}" + ssl: + # SSL/TLS settings for the transport layer + certificate: + # X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.) + reload: + # Enable/disable automatic SSL certificates reload + enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" + # Check interval in seconds for certificates reload + check_interval: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL:60}" # CoAP server parameters coap: diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java index 271866d23c..7882125906 100644 --- a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java @@ -25,10 +25,12 @@ import org.eclipse.californium.core.server.resources.Resource; import org.eclipse.californium.elements.config.Configuration; import org.eclipse.californium.scandium.DTLSConnector; import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.thingsboard.common.util.ThingsBoardExecutors; +import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; @@ -42,22 +44,41 @@ import static org.eclipse.californium.core.config.CoapConfig.DEFAULT_BLOCKWISE_S @Slf4j @Component @TbCoapServerComponent -public class DefaultCoapServerService implements CoapServerService { +public class DefaultCoapServerService implements CoapServerService, SmartInitializingSingleton { @Autowired private CoapServerContext coapServerContext; private CoapServer server; - private TbCoapDtlsCertificateVerifier tbDtlsCertificateVerifier; + private volatile TbCoapDtlsCertificateVerifier tbDtlsCertificateVerifier; private ScheduledExecutorService dtlsSessionsExecutor; + private volatile DTLSConnector dtlsConnector; + + private volatile CoapEndpoint dtlsCoapEndpoint; + @PostConstruct public void init() throws UnknownHostException { createCoapServer(); } + @Override + public void afterSingletonsInstantiated() { + if (isDtlsEnabled()) { + coapServerContext.getDtlsSettings().registerReloadCallback(() -> { + try { + log.info("CoAP DTLS certificates reloaded. Recreating DTLS endpoint..."); + recreateDtlsEndpoint(); + log.info("CoAP DTLS endpoint recreated successfully with new certificates."); + } catch (Exception e) { + log.error("Failed to recreate CoAP DTLS endpoint after certificate reload", e); + } + }); + } + } + @PreDestroy public void shutdown() { if (dtlsSessionsExecutor != null) { @@ -83,16 +104,7 @@ public class DefaultCoapServerService implements CoapServerService { } private CoapServer createCoapServer() throws UnknownHostException { - Configuration networkConfig = new Configuration(); - networkConfig.set(CoapConfig.BLOCKWISE_STRICT_BLOCK2_OPTION, true); - networkConfig.set(CoapConfig.BLOCKWISE_ENTITY_TOO_LARGE_AUTO_FAILOVER, true); - networkConfig.set(CoapConfig.BLOCKWISE_STATUS_LIFETIME, DEFAULT_BLOCKWISE_STATUS_LIFETIME_IN_SECONDS, TimeUnit.SECONDS); - networkConfig.set(CoapConfig.MAX_RESOURCE_BODY_SIZE, 256 * 1024 * 1024); - networkConfig.set(CoapConfig.RESPONSE_MATCHING, CoapConfig.MatcherMode.RELAXED); - networkConfig.set(CoapConfig.PREFERRED_BLOCK_SIZE, 1024); - networkConfig.set(CoapConfig.MAX_MESSAGE_SIZE, 1024); - networkConfig.set(CoapConfig.MAX_RETRANSMIT, 4); - networkConfig.set(CoapConfig.COAP_PORT, coapServerContext.getPort()); + Configuration networkConfig = createNetworkConfiguration(); server = new CoapServer(networkConfig); CoapEndpoint.Builder noSecCoapEndpointBuilder = new CoapEndpoint.Builder(); @@ -104,16 +116,7 @@ public class DefaultCoapServerService implements CoapServerService { CoapEndpoint noSecCoapEndpoint = noSecCoapEndpointBuilder.build(); server.addEndpoint(noSecCoapEndpoint); if (isDtlsEnabled()) { - CoapEndpoint.Builder dtlsCoapEndpointBuilder = new CoapEndpoint.Builder(); - TbCoapDtlsSettings dtlsSettings = coapServerContext.getDtlsSettings(); - DtlsConnectorConfig dtlsConnectorConfig = dtlsSettings.dtlsConnectorConfig(networkConfig); - networkConfig.set(CoapConfig.COAP_SECURE_PORT, dtlsConnectorConfig.getAddress().getPort()); - dtlsCoapEndpointBuilder.setConfiguration(networkConfig); - DTLSConnector connector = new DTLSConnector(dtlsConnectorConfig); - dtlsCoapEndpointBuilder.setConnector(connector); - CoapEndpoint dtlsCoapEndpoint = dtlsCoapEndpointBuilder.build(); - server.addEndpoint(dtlsCoapEndpoint); - tbDtlsCertificateVerifier = (TbCoapDtlsCertificateVerifier) dtlsConnectorConfig.getAdvancedCertificateVerifier(); + createDtlsEndpoint(networkConfig); dtlsSessionsExecutor = ThingsBoardExecutors.newSingleThreadScheduledExecutor(getClass().getSimpleName()); dtlsSessionsExecutor.scheduleAtFixedRate(this::evictTimeoutSessions, new Random().nextInt((int) getDtlsSessionReportTimeout()), getDtlsSessionReportTimeout(), TimeUnit.MILLISECONDS); } @@ -137,4 +140,88 @@ public class DefaultCoapServerService implements CoapServerService { return tbDtlsCertificateVerifier.getDtlsSessionReportTimeout(); } + private Configuration createNetworkConfiguration() { + Configuration networkConfig = new Configuration(); + networkConfig.set(CoapConfig.BLOCKWISE_STRICT_BLOCK2_OPTION, true); + networkConfig.set(CoapConfig.BLOCKWISE_ENTITY_TOO_LARGE_AUTO_FAILOVER, true); + networkConfig.set(CoapConfig.BLOCKWISE_STATUS_LIFETIME, DEFAULT_BLOCKWISE_STATUS_LIFETIME_IN_SECONDS, TimeUnit.SECONDS); + networkConfig.set(CoapConfig.MAX_RESOURCE_BODY_SIZE, 256 * 1024 * 1024); + networkConfig.set(CoapConfig.RESPONSE_MATCHING, CoapConfig.MatcherMode.RELAXED); + networkConfig.set(CoapConfig.PREFERRED_BLOCK_SIZE, 1024); + networkConfig.set(CoapConfig.MAX_MESSAGE_SIZE, 1024); + networkConfig.set(CoapConfig.MAX_RETRANSMIT, 4); + networkConfig.set(CoapConfig.COAP_PORT, coapServerContext.getPort()); + return networkConfig; + } + + DtlsConnectorConfig buildDtlsConnectorConfig(Configuration networkConfig) throws UnknownHostException { + TbCoapDtlsSettings dtlsSettings = coapServerContext.getDtlsSettings(); + DtlsConnectorConfig dtlsConnectorConfig = dtlsSettings.dtlsConnectorConfig(networkConfig); + networkConfig.set(CoapConfig.COAP_SECURE_PORT, dtlsConnectorConfig.getAddress().getPort()); + return dtlsConnectorConfig; + } + + CoapEndpoint buildDtlsEndpoint(Configuration networkConfig, DTLSConnector connector) { + CoapEndpoint.Builder dtlsCoapEndpointBuilder = new CoapEndpoint.Builder(); + dtlsCoapEndpointBuilder.setConfiguration(networkConfig); + dtlsCoapEndpointBuilder.setConnector(connector); + return dtlsCoapEndpointBuilder.build(); + } + + private void createDtlsEndpoint(Configuration networkConfig) throws UnknownHostException { + DtlsConnectorConfig dtlsConnectorConfig = buildDtlsConnectorConfig(networkConfig); + DTLSConnector newConnector = createDtlsConnector(dtlsConnectorConfig); + CoapEndpoint newEndpoint = buildDtlsEndpoint(networkConfig, newConnector); + server.addEndpoint(newEndpoint); + + dtlsConnector = newConnector; + dtlsCoapEndpoint = newEndpoint; + tbDtlsCertificateVerifier = (TbCoapDtlsCertificateVerifier) dtlsConnectorConfig.getAdvancedCertificateVerifier(); + } + + DTLSConnector createDtlsConnector(DtlsConnectorConfig config) { + return new DTLSConnector(config); + } + + private synchronized void recreateDtlsEndpoint() throws IOException { + CoapEndpoint oldDtlsEndpoint = dtlsCoapEndpoint; + DTLSConnector oldDtlsConnector = dtlsConnector; + + Configuration networkConfig = createNetworkConfiguration(); + + log.info("Creating new DTLS endpoint with updated certificates..."); + + DtlsConnectorConfig dtlsConnectorConfig = buildDtlsConnectorConfig(networkConfig); + DTLSConnector newConnector = createDtlsConnector(dtlsConnectorConfig); + CoapEndpoint newEndpoint = buildDtlsEndpoint(networkConfig, newConnector); + + server.addEndpoint(newEndpoint); + try { + newEndpoint.start(); + } catch (IOException e) { + log.error("Failed to start new DTLS endpoint, cleaning up", e); + server.getEndpoints().remove(newEndpoint); + newEndpoint.destroy(); + newConnector.destroy(); + throw e; + } + log.info("New DTLS endpoint started successfully."); + + // Only swap instance fields after a successful start + dtlsConnector = newConnector; + dtlsCoapEndpoint = newEndpoint; + tbDtlsCertificateVerifier = (TbCoapDtlsCertificateVerifier) dtlsConnectorConfig.getAdvancedCertificateVerifier(); + + if (oldDtlsEndpoint != null) { + log.info("Stopping old DTLS endpoint..."); + oldDtlsEndpoint.stop(); + if (oldDtlsConnector != null) { + oldDtlsConnector.destroy(); + } + oldDtlsEndpoint.destroy(); + server.getEndpoints().remove(oldDtlsEndpoint); + log.info("Old DTLS endpoint stopped and destroyed."); + } + } + } diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java index 672de6bacd..c6e90d1351 100644 --- a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java @@ -100,6 +100,10 @@ public class TbCoapDtlsSettings { @Autowired(required = false) private TbServiceInfoProvider serviceInfoProvider; + public void registerReloadCallback(Runnable callback) { + coapDtlsCredentialsConfig.registerReloadCallback(callback); + } + public DtlsConnectorConfig dtlsConnectorConfig(Configuration configuration) throws UnknownHostException { DtlsConnectorConfig.Builder configBuilder = new DtlsConnectorConfig.Builder(configuration); configBuilder.setAddress(getInetSocketAddress()); @@ -154,5 +158,5 @@ public class TbCoapDtlsSettings { } return null; } -} +} diff --git a/common/coap-server/src/test/java/org/thingsboard/server/coapserver/CoapDtlsCertificateReloadTest.java b/common/coap-server/src/test/java/org/thingsboard/server/coapserver/CoapDtlsCertificateReloadTest.java new file mode 100644 index 0000000000..642f2e0be9 --- /dev/null +++ b/common/coap-server/src/test/java/org/thingsboard/server/coapserver/CoapDtlsCertificateReloadTest.java @@ -0,0 +1,235 @@ +/** + * 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.coapserver; + +import org.eclipse.californium.core.CoapServer; +import org.eclipse.californium.core.network.CoapEndpoint; +import org.eclipse.californium.core.network.Endpoint; +import org.eclipse.californium.elements.config.Configuration; +import org.eclipse.californium.scandium.DTLSConnector; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.test.util.ReflectionTestUtils; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class CoapDtlsCertificateReloadTest { + + @Mock + private CoapServerContext mockCoapServerContext; + + @Mock + private TbCoapDtlsSettings mockDtlsSettings; + + @Mock + private CoapServer mockCoapServer; + + @Mock + private CoapEndpoint mockDtlsEndpoint; + + @Mock + private DTLSConnector mockDtlsConnector; + + private DefaultCoapServerService coapServerService; + + @BeforeEach + public void setup() { + coapServerService = new DefaultCoapServerService(); + ReflectionTestUtils.setField(coapServerService, "coapServerContext", mockCoapServerContext); + + when(mockCoapServerContext.getHost()).thenReturn("localhost"); + when(mockCoapServerContext.getPort()).thenReturn(5683); + doAnswer(invocation -> { + invocation.getArgument(0); + return null; + }).when(mockDtlsSettings).registerReloadCallback(any()); + } + + @Test + public void givenDtlsEnabled_whenRegisterCertificateReloadCallback_thenShouldRegisterCallback() { + when(mockCoapServerContext.getDtlsSettings()).thenReturn(mockDtlsSettings); + + ReflectionTestUtils.setField(coapServerService, "server", mockCoapServer); + + ReflectionTestUtils.invokeMethod(coapServerService, "afterSingletonsInstantiated"); + + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mockDtlsSettings).registerReloadCallback(callbackCaptor.capture()); + assertThat(callbackCaptor.getValue()).isNotNull(); + } + + @Test + public void givenDtlsNotEnabled_whenRegisterCertificateReloadCallback_thenShouldNotRegisterCallback() { + when(mockCoapServerContext.getDtlsSettings()).thenReturn(null); + + ReflectionTestUtils.invokeMethod(coapServerService, "afterSingletonsInstantiated"); + + verify(mockDtlsSettings, never()).registerReloadCallback(any()); + } + + @Test + public void givenReloadCallbackInvoked_whenNewEndpointCreationFails_thenOldEndpointIsPreserved() { + when(mockCoapServerContext.getDtlsSettings()).thenReturn(mockDtlsSettings); + + ReflectionTestUtils.setField(coapServerService, "server", mockCoapServer); + ReflectionTestUtils.setField(coapServerService, "dtlsCoapEndpoint", mockDtlsEndpoint); + ReflectionTestUtils.setField(coapServerService, "dtlsConnector", mockDtlsConnector); + + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + ReflectionTestUtils.invokeMethod(coapServerService, "afterSingletonsInstantiated"); + verify(mockDtlsSettings).registerReloadCallback(callbackCaptor.capture()); + + Runnable reloadCallback = callbackCaptor.getValue(); + // dtlsSettings.dtlsConnectorConfig() isn't mocked, so the callback will throw. + // The old endpoint should not be stopped/destroyed when creation of the new one fails. + reloadCallback.run(); + + verify(mockDtlsEndpoint, never()).stop(); + verify(mockDtlsConnector, never()).destroy(); + } + + @Test + public void givenDtlsEnabled_whenInit_thenShouldRegisterCallback() { + when(mockCoapServerContext.getDtlsSettings()).thenReturn(mockDtlsSettings); + when(mockCoapServerContext.getHost()).thenReturn("localhost"); + when(mockCoapServerContext.getPort()).thenReturn(5683); + + ReflectionTestUtils.setField(coapServerService, "server", mockCoapServer); + ReflectionTestUtils.invokeMethod(coapServerService, "afterSingletonsInstantiated"); + + verify(mockDtlsSettings).registerReloadCallback(any(Runnable.class)); + } + + @Test + public void givenReloadCallback_whenInvokedMultipleTimes_thenShouldRegisterOnce() { + when(mockCoapServerContext.getDtlsSettings()).thenReturn(mockDtlsSettings); + ReflectionTestUtils.setField(coapServerService, "server", mockCoapServer); + ReflectionTestUtils.setField(coapServerService, "dtlsCoapEndpoint", mockDtlsEndpoint); + ReflectionTestUtils.setField(coapServerService, "dtlsConnector", mockDtlsConnector); + + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + ReflectionTestUtils.invokeMethod(coapServerService, "afterSingletonsInstantiated"); + verify(mockDtlsSettings).registerReloadCallback(callbackCaptor.capture()); + + Runnable reloadCallback = callbackCaptor.getValue(); + assertThat(reloadCallback).isNotNull(); + } + + @Test + public void givenReloadCallback_whenSuccessful_thenOldEndpointRemovedFromServer() throws Exception { + // GIVEN + when(mockCoapServerContext.getDtlsSettings()).thenReturn(mockDtlsSettings); + + CoapEndpoint mockNewEndpoint = mock(CoapEndpoint.class); + DTLSConnector mockNewConnector = mock(DTLSConnector.class); + DtlsConnectorConfig mockDtlsConfig = mock(DtlsConnectorConfig.class); + TbCoapDtlsCertificateVerifier mockNewVerifier = mock(TbCoapDtlsCertificateVerifier.class); + when(mockDtlsConfig.getAdvancedCertificateVerifier()).thenReturn(mockNewVerifier); + + DefaultCoapServerService spyService = Mockito.spy(coapServerService); + ReflectionTestUtils.setField(spyService, "coapServerContext", mockCoapServerContext); + ReflectionTestUtils.setField(spyService, "server", mockCoapServer); + ReflectionTestUtils.setField(spyService, "dtlsCoapEndpoint", mockDtlsEndpoint); + ReflectionTestUtils.setField(spyService, "dtlsConnector", mockDtlsConnector); + + doReturn(mockDtlsConfig).when(spyService).buildDtlsConnectorConfig(any(Configuration.class)); + doReturn(mockNewConnector).when(spyService).createDtlsConnector(any(DtlsConnectorConfig.class)); + doReturn(mockNewEndpoint).when(spyService).buildDtlsEndpoint(any(Configuration.class), any(DTLSConnector.class)); + + List endpointsList = new CopyOnWriteArrayList<>(); + endpointsList.add(mockDtlsEndpoint); + when(mockCoapServer.getEndpoints()).thenReturn(endpointsList); + + // WHEN + ReflectionTestUtils.invokeMethod(spyService, "recreateDtlsEndpoint"); + + // THEN + assertThat(endpointsList).doesNotContain(mockDtlsEndpoint); + verify(mockDtlsEndpoint).stop(); + verify(mockDtlsEndpoint).destroy(); + verify(mockDtlsConnector).destroy(); + verify(mockCoapServer).addEndpoint(mockNewEndpoint); + verify(mockNewEndpoint).start(); + assertThat(ReflectionTestUtils.getField(spyService, "dtlsCoapEndpoint")).isSameAs(mockNewEndpoint); + } + + @Test + public void givenReloadCallback_whenStartFails_thenNewResourcesCleaned() throws Exception { + // GIVEN + when(mockCoapServerContext.getDtlsSettings()).thenReturn(mockDtlsSettings); + + CoapEndpoint mockNewEndpoint = mock(CoapEndpoint.class); + DTLSConnector mockNewConnector = mock(DTLSConnector.class); + DtlsConnectorConfig mockDtlsConfig = mock(DtlsConnectorConfig.class); + + doThrow(new IOException("start failed")).when(mockNewEndpoint).start(); + + DefaultCoapServerService spyService = Mockito.spy(coapServerService); + ReflectionTestUtils.setField(spyService, "coapServerContext", mockCoapServerContext); + ReflectionTestUtils.setField(spyService, "server", mockCoapServer); + ReflectionTestUtils.setField(spyService, "dtlsCoapEndpoint", mockDtlsEndpoint); + ReflectionTestUtils.setField(spyService, "dtlsConnector", mockDtlsConnector); + + doReturn(mockDtlsConfig).when(spyService).buildDtlsConnectorConfig(any(Configuration.class)); + doReturn(mockNewConnector).when(spyService).createDtlsConnector(any(DtlsConnectorConfig.class)); + doReturn(mockNewEndpoint).when(spyService).buildDtlsEndpoint(any(Configuration.class), any(DTLSConnector.class)); + + List endpointsList = new CopyOnWriteArrayList<>(); + when(mockCoapServer.getEndpoints()).thenReturn(endpointsList); + + // WHEN - the callback catches the IOException internally + spyService.afterSingletonsInstantiated(); + + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mockDtlsSettings).registerReloadCallback(callbackCaptor.capture()); + Runnable reloadCallback = callbackCaptor.getValue(); + reloadCallback.run(); + + // THEN - new resources cleaned up + verify(mockNewEndpoint).destroy(); + verify(mockNewConnector).destroy(); + assertThat(endpointsList).doesNotContain(mockNewEndpoint); + // Old fields preserved + assertThat(ReflectionTestUtils.getField(spyService, "dtlsCoapEndpoint")).isSameAs(mockDtlsEndpoint); + assertThat(ReflectionTestUtils.getField(spyService, "dtlsConnector")).isSameAs(mockDtlsConnector); + // Old endpoint not touched + verify(mockDtlsEndpoint, never()).stop(); + verify(mockDtlsEndpoint, never()).destroy(); + } + +} diff --git a/common/coap-server/src/test/java/org/thingsboard/server/coapserver/TbCoapDtlsSettingsTest.java b/common/coap-server/src/test/java/org/thingsboard/server/coapserver/TbCoapDtlsSettingsTest.java index c75348d97e..9538670e25 100644 --- a/common/coap-server/src/test/java/org/thingsboard/server/coapserver/TbCoapDtlsSettingsTest.java +++ b/common/coap-server/src/test/java/org/thingsboard/server/coapserver/TbCoapDtlsSettingsTest.java @@ -18,8 +18,8 @@ package org.thingsboard.server.coapserver; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; @@ -41,11 +41,11 @@ class TbCoapDtlsSettingsTest { @Autowired TbCoapDtlsSettings coapDtlsSettings; - @MockBean + @MockitoBean SslCredentialsConfig sslCredentialsConfig; - @MockBean + @MockitoBean private TransportService transportService; - @MockBean + @MockitoBean private TbServiceInfoProvider serviceInfoProvider; @Test diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceUtils.java index 725aa7921c..626d0cd372 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceUtils.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceUtils.java @@ -51,11 +51,9 @@ public class ResourceUtils { return true; } else { try { - URL url = Resources.getResource(path); - if (url != null) { - return true; - } - } catch (IllegalArgumentException e) {} + Resources.getResource(path); + return true; + } catch (IllegalArgumentException ignored) {} } return false; } @@ -93,9 +91,9 @@ public class ResourceUtils { } } catch (Exception e) { if (e instanceof NullPointerException) { - log.warn("Unable to find resource: " + filePath); + log.warn("Unable to find resource: {}", filePath); } else { - log.warn("Unable to find resource: " + filePath, e); + log.warn("Unable to find resource: {}", filePath, e); } } throw new RuntimeException("Unable to find resource: " + filePath); @@ -113,15 +111,16 @@ public class ResourceUtils { return resourceFile.getAbsolutePath(); } else { URL url = classLoader.getResource(filePath); - return url.toURI().toString(); + return url != null ? url.toURI().toString() : null; } } catch (Exception e) { if (e instanceof NullPointerException) { - log.warn("Unable to find resource: " + filePath); + log.warn("Unable to find resource: {}", filePath); } else { - log.warn("Unable to find resource: " + filePath, e); + log.warn("Unable to find resource: {}", filePath, e); } throw new RuntimeException("Unable to find resource: " + filePath); } } + } diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/ResourceUtilsTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/ResourceUtilsTest.java new file mode 100644 index 0000000000..8c91762068 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/ResourceUtilsTest.java @@ -0,0 +1,39 @@ +/** + * 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.common.data; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ResourceUtilsTest { + + @Test + public void givenNonExistentResource_whenGetUri_thenReturnsNull() { + String result = ResourceUtils.getUri(ResourceUtilsTest.class.getClassLoader(), "non/existent/resource/path.txt"); + + assertThat(result).isNull(); + } + + @Test + public void givenExistingClasspathResource_whenGetUri_thenReturnsNonNullUri() { + String result = ResourceUtils.getUri(ResourceUtilsTest.class.getClassLoader(), "org/thingsboard/server/common/data/ResourceUtilsTest.class"); + + assertThat(result).isNotNull(); + assertThat(result).contains("ResourceUtilsTest"); + } + +} diff --git a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java index 9a7cfe43ff..73d77e2ddb 100644 --- a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java +++ b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java @@ -76,10 +76,6 @@ import java.util.List; import java.util.UUID; import java.util.function.Consumer; - -/** - * @author Andrew Shvayka - */ @RestController @ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.http.enabled}'=='true')") @RequestMapping("/api/v1") diff --git a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/HttpTransportContext.java b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/HttpTransportContext.java index 830e081dfd..db72ba747c 100644 --- a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/HttpTransportContext.java +++ b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/HttpTransportContext.java @@ -26,9 +26,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.thingsboard.server.common.transport.TransportContext; -/** - * Created by ashvayka on 04.10.18. - */ @Slf4j @ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.http.enabled}'=='true')") @Component @@ -52,4 +49,5 @@ public class HttpTransportContext extends TransportContext { } }; } + } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java index 412036677a..78f292d69c 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java @@ -29,6 +29,7 @@ import org.eclipse.leshan.server.californium.bootstrap.LwM2mBootstrapPskStore; import org.eclipse.leshan.server.californium.bootstrap.endpoint.CaliforniumBootstrapServerEndpointsProvider; import org.eclipse.leshan.server.californium.bootstrap.endpoint.coap.CoapBootstrapServerProtocolProvider; import org.eclipse.leshan.server.californium.bootstrap.endpoint.coaps.CoapsBootstrapServerProtocolProvider; +import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.stereotype.Component; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.config.ssl.SslCredentials; @@ -55,7 +56,7 @@ import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.se @Component @TbLwM2mBootstrapTransportComponent @RequiredArgsConstructor -public class LwM2MTransportBootstrapService { +public class LwM2MTransportBootstrapService implements SmartInitializingSingleton { private final LwM2MTransportServerConfig serverConfig; private final LwM2MTransportBootstrapConfig bootstrapConfig; @@ -65,6 +66,19 @@ public class LwM2MTransportBootstrapService { private final TbLwM2MDtlsBootstrapCertificateVerifier certificateVerifier; private LeshanBootstrapServer server; + @Override + public void afterSingletonsInstantiated() { + bootstrapConfig.registerServerReloadCallback(() -> { + try { + log.info("LwM2M Bootstrap certificates reloaded. Recreating bootstrap server..."); + recreateBootstrapServer(); + log.info("LwM2M Bootstrap server recreated successfully with new certificates."); + } catch (Exception e) { + log.error("Failed to recreate LwM2M Bootstrap server after certificate reload", e); + } + }); + } + @PostConstruct public void init() { log.info("Starting LwM2M transport bootstrap server..."); @@ -110,7 +124,7 @@ public class LwM2MTransportBootstrapService { // Create Californium Configuration Configuration serverCoapConfig = endpointsBuilder.createDefaultConfiguration(); - getCoapConfig(serverCoapConfig, bootstrapConfig.getPort(), bootstrapConfig.getSecurePort(),serverConfig); + getCoapConfig(serverCoapConfig, bootstrapConfig.getPort(), bootstrapConfig.getSecurePort(), serverConfig); serverCoapConfig.setTransient(DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY); serverCoapConfig.set(DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, serverConfig.isRecommendedCiphers()); serverCoapConfig.setTransient(DtlsConfig.DTLS_CONNECTION_ID_LENGTH); @@ -119,7 +133,7 @@ public class LwM2MTransportBootstrapService { serverCoapConfig.set(DTLS_RETRANSMISSION_TIMEOUT, serverConfig.getDtlsRetransmissionTimeout(), MILLISECONDS); if (serverConfig.getDtlsCidLength() != null) { - setDtlsConnectorConfigCidLength( serverCoapConfig, serverConfig.getDtlsCidLength()); + setDtlsConnectorConfigCidLength(serverCoapConfig, serverConfig.getDtlsCidLength()); } /* Create DTLS Config */ @@ -164,4 +178,27 @@ public class LwM2MTransportBootstrapService { builder.setTrustedCertificates(new X509Certificate[0]); } } + + private synchronized void recreateBootstrapServer() { + LeshanBootstrapServer oldServer = this.server; + + log.info("Creating new LwM2M Bootstrap server with updated certificates..."); + LeshanBootstrapServer newServer = getLhBootstrapServer(); + try { + newServer.start(); + } catch (Exception e) { + log.error("Failed to start new LwM2M Bootstrap server, rolling back", e); + newServer.destroy(); + throw e; + } + this.server = newServer; + log.info("New LwM2M Bootstrap server started successfully."); + + if (oldServer != null) { + log.info("Stopping old LwM2M Bootstrap server..."); + oldServer.destroy(); + log.info("Old LwM2M Bootstrap server stopped."); + } + } + } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportBootstrapConfig.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportBootstrapConfig.java index b15a757ae3..d3bf9e33fe 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportBootstrapConfig.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportBootstrapConfig.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.transport.lwm2m.config; +import jakarta.annotation.PostConstruct; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -27,6 +28,9 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.transport.config.ssl.SslCredentials; import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + @Slf4j @Component @ConditionalOnExpression("'${service.type:null}'=='tb-transport' || '${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core'") @@ -62,8 +66,33 @@ public class LwM2MTransportBootstrapConfig implements LwM2MSecureServerConfig { @Qualifier("lwm2mBootstrapCredentials") private SslCredentialsConfig credentialsConfig; + private final List serverReloadCallbacks = new CopyOnWriteArrayList<>(); + + @PostConstruct + public void init() { + credentialsConfig.registerReloadCallback(() -> { + log.info("LwM2M Bootstrap DTLS certificates reloaded. Triggering bootstrap server reload..."); + notifyServerReload(); + }); + } + + public void registerServerReloadCallback(Runnable callback) { + serverReloadCallbacks.add(callback); + } + + private void notifyServerReload() { + for (Runnable callback : serverReloadCallbacks) { + try { + callback.run(); + } catch (Exception e) { + log.error("Error executing LwM2M bootstrap server reload callback", e); + } + } + } + @Override public SslCredentials getSslCredentials() { return this.credentialsConfig.getCredentials(); } + } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java index d2b24fe9a8..6034219c4d 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.transport.lwm2m.config; +import jakarta.annotation.PostConstruct; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -31,6 +32,7 @@ import org.thingsboard.server.common.transport.config.ssl.SslCredentials; import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; @Slf4j @Component @@ -134,6 +136,35 @@ public class LwM2MTransportServerConfig implements LwM2MSecureServerConfig { @Qualifier("lwm2mTrustCredentials") private SslCredentialsConfig trustCredentialsConfig; + private final List serverReloadCallbacks = new CopyOnWriteArrayList<>(); + + @PostConstruct + public void init() { + credentialsConfig.registerReloadCallback(() -> { + log.info("LwM2M Server DTLS certificates reloaded. Triggering server reload..."); + notifyServerReload(); + }); + + trustCredentialsConfig.registerReloadCallback(() -> { + log.info("LwM2M Trust certificates reloaded. Triggering server reload..."); + notifyServerReload(); + }); + } + + public void registerServerReloadCallback(Runnable callback) { + serverReloadCallbacks.add(callback); + } + + private void notifyServerReload() { + for (Runnable callback : serverReloadCallbacks) { + try { + callback.run(); + } catch (Exception e) { + log.error("Error executing LwM2M server reload callback", e); + } + } + } + @Override public SslCredentials getSslCredentials() { return this.credentialsConfig.getCredentials(); @@ -142,4 +173,5 @@ public class LwM2MTransportServerConfig implements LwM2MSecureServerConfig { public SslCredentials getTrustSslCredentials() { return this.trustCredentialsConfig.getCredentials(); } + } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java index c4fb504864..21fdc07277 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java @@ -33,6 +33,7 @@ import org.eclipse.leshan.server.californium.endpoint.CaliforniumServerEndpoints import org.eclipse.leshan.server.californium.endpoint.coap.CoapServerProtocolProvider; import org.eclipse.leshan.server.californium.endpoint.coaps.CoapsServerProtocolProvider; import org.eclipse.leshan.server.registration.RegistrationStore; +import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.context.annotation.DependsOn; import org.springframework.stereotype.Component; import org.thingsboard.server.cache.ota.OtaPackageDataCache; @@ -68,7 +69,7 @@ import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.se @DependsOn({"lwM2mDownlinkMsgHandler", "lwM2mUplinkMsgHandler"}) @TbLwM2mTransportComponent @RequiredArgsConstructor -public class DefaultLwM2mTransportService implements LwM2MTransportService { +public class DefaultLwM2mTransportService implements LwM2MTransportService, SmartInitializingSingleton { public static final CipherSuite[] RPK_OR_X509_CIPHER_SUITES = {TLS_PSK_WITH_AES_128_CCM_8, TLS_PSK_WITH_AES_128_CBC_SHA256, TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256}; public static final CipherSuite[] PSK_CIPHER_SUITES = {TLS_PSK_WITH_AES_128_CCM_8, TLS_PSK_WITH_AES_128_CBC_SHA256}; @@ -84,6 +85,20 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { private final LwM2mVersionedModelProvider modelProvider; private LeshanServer server; + private LwM2mServerListener serverListener; + + @Override + public void afterSingletonsInstantiated() { + config.registerServerReloadCallback(() -> { + try { + log.info("LwM2M certificates reloaded. Recreating LwM2M server..."); + recreateLwM2mServer(); + log.info("LwM2M server recreated successfully with new certificates."); + } catch (Exception e) { + log.error("Failed to recreate LwM2M server after certificate reload", e); + } + }); + } @AfterStartUp(order = AfterStartUp.AFTER_TRANSPORT_SERVICE) public void init() { @@ -95,11 +110,11 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { private void startLhServer() { log.info("Starting LwM2M transport server..."); this.server.start(); - LwM2mServerListener lhServerCertListener = new LwM2mServerListener(handler); - this.server.getRegistrationService().addListener(lhServerCertListener.registrationListener); - this.server.getPresenceService().addListener(lhServerCertListener.presenceListener); - this.server.getObservationService().addListener(lhServerCertListener.observationListener); - this.server.getSendService().addListener(lhServerCertListener.sendListener); + serverListener = new LwM2mServerListener(handler); + this.server.getRegistrationService().addListener(serverListener.registrationListener); + this.server.getPresenceService().addListener(serverListener.presenceListener); + this.server.getObservationService().addListener(serverListener.observationListener); + this.server.getSendService().addListener(serverListener.sendListener); log.info("Started LwM2M transport server."); } @@ -214,6 +229,44 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { } } + private synchronized void recreateLwM2mServer() { + LeshanServer oldServer = this.server; + LwM2mServerListener oldListener = this.serverListener; + + log.info("Creating new LwM2M server with updated certificates..."); + LeshanServer newServer = getLhServer(); + newServer.start(); + + try { + LwM2mServerListener newListener = new LwM2mServerListener(handler); + newServer.getRegistrationService().addListener(newListener.registrationListener); + newServer.getPresenceService().addListener(newListener.presenceListener); + newServer.getObservationService().addListener(newListener.observationListener); + newServer.getSendService().addListener(newListener.sendListener); + + this.server = newServer; + this.context.setServer(newServer); + this.serverListener = newListener; + } catch (Exception e) { + log.error("Failed to register listeners on new LwM2M server, rolling back", e); + newServer.destroy(); + throw e; + } + log.info("New LwM2M server started successfully."); + + if (oldServer != null) { + log.info("Stopping old LwM2M server..."); + if (oldListener != null) { + oldServer.getRegistrationService().removeListener(oldListener.registrationListener); + oldServer.getPresenceService().removeListener(oldListener.presenceListener); + oldServer.getObservationService().removeListener(oldListener.observationListener); + oldServer.getSendService().removeListener(oldListener.sendListener); + } + oldServer.destroy(); + log.info("Old LwM2M server stopped."); + } + } + @Override public String getName() { return DataConstants.LWM2M_TRANSPORT_NAME; diff --git a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2mBootstrapCertificateReloadTest.java b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2mBootstrapCertificateReloadTest.java new file mode 100644 index 0000000000..51d37ca739 --- /dev/null +++ b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2mBootstrapCertificateReloadTest.java @@ -0,0 +1,192 @@ +/** + * 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.transport.lwm2m.bootstrap; + +import org.eclipse.leshan.server.bootstrap.LeshanBootstrapServer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.common.transport.config.ssl.SslCredentials; +import org.thingsboard.server.transport.lwm2m.bootstrap.secure.TbLwM2MDtlsBootstrapCertificateVerifier; +import org.thingsboard.server.transport.lwm2m.bootstrap.store.LwM2MBootstrapSecurityStore; +import org.thingsboard.server.transport.lwm2m.bootstrap.store.LwM2MInMemoryBootstrapConfigStore; +import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportBootstrapConfig; +import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class LwM2mBootstrapCertificateReloadTest { + + @Mock + private LwM2MTransportServerConfig mockServerConfig; + + @Mock + private LwM2MTransportBootstrapConfig mockBootstrapConfig; + + @Mock + private LwM2MBootstrapSecurityStore mockSecurityStore; + + @Mock + private LwM2MInMemoryBootstrapConfigStore mockConfigStore; + + @Mock + private TransportService mockTransportService; + + @Mock + private TbLwM2MDtlsBootstrapCertificateVerifier mockCertificateVerifier; + + @Mock + private LeshanBootstrapServer mockBootstrapServer; + + @Mock + private SslCredentials mockSslCredentials; + + private LwM2MTransportBootstrapService bootstrapService; + + @BeforeEach + public void setup() { + bootstrapService = new LwM2MTransportBootstrapService( + mockServerConfig, + mockBootstrapConfig, + mockSecurityStore, + mockConfigStore, + mockTransportService, + mockCertificateVerifier + ); + + when(mockBootstrapConfig.getHost()).thenReturn("localhost"); + when(mockBootstrapConfig.getPort()).thenReturn(5687); + when(mockBootstrapConfig.getSecureHost()).thenReturn("localhost"); + when(mockBootstrapConfig.getSecurePort()).thenReturn(5688); + when(mockBootstrapConfig.getSslCredentials()).thenReturn(mockSslCredentials); + when(mockServerConfig.getDtlsRetransmissionTimeout()).thenReturn(9000); + } + + @Test + public void givenInit_whenCalled_thenShouldRegisterCertificateReloadCallback() { + ReflectionTestUtils.setField(bootstrapService, "server", mockBootstrapServer); + + bootstrapService.afterSingletonsInstantiated(); + + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mockBootstrapConfig).registerServerReloadCallback(callbackCaptor.capture()); + + assertThat(callbackCaptor.getValue()).isNotNull(); + } + + @Test + public void givenReloadCallback_whenNewServerCreationFails_thenOldServerIsPreserved() { + ReflectionTestUtils.setField(bootstrapService, "server", mockBootstrapServer); + + // Force getLhBootstrapServer() to fail by returning null host (causes InetSocketAddress to throw) + when(mockBootstrapConfig.getHost()).thenReturn(null); + + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + bootstrapService.afterSingletonsInstantiated(); + verify(mockBootstrapConfig).registerServerReloadCallback(callbackCaptor.capture()); + + Runnable reloadCallback = callbackCaptor.getValue(); + + // getLhBootstrapServer() will fail due to null host. + // With create-then-swap, the old server should NOT be destroyed. + reloadCallback.run(); + + verify(mockBootstrapServer, never()).destroy(); + assertThat(ReflectionTestUtils.getField(bootstrapService, "server")).isSameAs(mockBootstrapServer); + } + + @Test + public void givenNullServer_whenRecreate_thenShouldNotThrow() { + ReflectionTestUtils.setField(bootstrapService, "server", null); + + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + bootstrapService.afterSingletonsInstantiated(); + verify(mockBootstrapConfig).registerServerReloadCallback(callbackCaptor.capture()); + + Runnable reloadCallback = callbackCaptor.getValue(); + + // Should not throw — callback catches exceptions internally + reloadCallback.run(); + } + + @Test + public void givenCertificateUpdate_whenRecreate_thenShouldUseNewCredentials() { + SslCredentials oldCredentials = mockSslCredentials; + SslCredentials newCredentials = mock(SslCredentials.class); + + when(mockBootstrapConfig.getSslCredentials()).thenReturn(oldCredentials).thenReturn(newCredentials); + + SslCredentials firstCall = mockBootstrapConfig.getSslCredentials(); + assertThat(firstCall).isEqualTo(oldCredentials); + + SslCredentials secondCall = mockBootstrapConfig.getSslCredentials(); + assertThat(secondCall).isEqualTo(newCredentials); + + verify(mockBootstrapConfig, times(2)).getSslCredentials(); + } + + @Test + public void givenReloadCallback_whenRegistered_thenShouldRegisterExactlyOne() { + bootstrapService.afterSingletonsInstantiated(); + + verify(mockBootstrapConfig, times(1)).registerServerReloadCallback(any()); + } + + @Test + public void givenReloadCallback_whenNewServerStartFails_thenNewServerDestroyedAndOldPreserved() { + // GIVEN + ReflectionTestUtils.setField(bootstrapService, "server", mockBootstrapServer); + + LeshanBootstrapServer mockNewServer = mock(LeshanBootstrapServer.class); + doThrow(new RuntimeException("start failed")).when(mockNewServer).start(); + + LwM2MTransportBootstrapService spyService = Mockito.spy(bootstrapService); + doReturn(mockNewServer).when(spyService).getLhBootstrapServer(); + + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + spyService.afterSingletonsInstantiated(); + verify(mockBootstrapConfig).registerServerReloadCallback(callbackCaptor.capture()); + + Runnable reloadCallback = callbackCaptor.getValue(); + + // WHEN + reloadCallback.run(); + + // THEN + verify(mockNewServer).destroy(); + assertThat(ReflectionTestUtils.getField(spyService, "server")).isSameAs(mockBootstrapServer); + verify(mockBootstrapServer, never()).destroy(); + } + +} diff --git a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerCertificateReloadTest.java b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerCertificateReloadTest.java new file mode 100644 index 0000000000..15e4d07622 --- /dev/null +++ b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerCertificateReloadTest.java @@ -0,0 +1,188 @@ +/** + * 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.transport.lwm2m.server; + +import org.eclipse.leshan.server.LeshanServer; +import org.eclipse.leshan.server.observation.ObservationService; +import org.eclipse.leshan.server.registration.RegistrationService; +import org.eclipse.leshan.server.registration.RegistrationStore; +import org.eclipse.leshan.server.send.SendService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.cache.ota.OtaPackageDataCache; +import org.thingsboard.server.common.transport.config.ssl.SslCredentials; +import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; +import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MAuthorizer; +import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MDtlsCertificateVerifier; +import org.thingsboard.server.transport.lwm2m.server.store.TbSecurityStore; +import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class LwM2mServerCertificateReloadTest { + + @Mock + private LwM2mTransportContext mockContext; + + @Mock + private LwM2MTransportServerConfig mockConfig; + + @Mock + private OtaPackageDataCache mockOtaCache; + + @Mock + private LwM2mUplinkMsgHandler mockHandler; + + @Mock + private RegistrationStore mockRegistrationStore; + + @Mock + private TbSecurityStore mockSecurityStore; + + @Mock + private TbLwM2MDtlsCertificateVerifier mockCertificateVerifier; + + @Mock + private TbLwM2MAuthorizer mockAuthorizer; + + @Mock + private LwM2mVersionedModelProvider mockModelProvider; + + @Mock + private LeshanServer mockLeshanServer; + + @Mock + private RegistrationService mockRegistrationService; + + @Mock + private ObservationService mockObservationService; + + @Mock + private SendService mockSendService; + + @Mock + private SslCredentials mockSslCredentials; + + private DefaultLwM2mTransportService lwm2mTransportService; + + @BeforeEach + public void setup() { + lwm2mTransportService = new DefaultLwM2mTransportService( + mockContext, + mockConfig, + mockOtaCache, + mockHandler, + mockRegistrationStore, + mockSecurityStore, + mockCertificateVerifier, + mockAuthorizer, + mockModelProvider + ); + + when(mockConfig.getHost()).thenReturn("localhost"); + when(mockConfig.getPort()).thenReturn(5683); + when(mockConfig.getSecureHost()).thenReturn("localhost"); + when(mockConfig.getSecurePort()).thenReturn(5684); + when(mockConfig.getSslCredentials()).thenReturn(mockSslCredentials); + + when(mockLeshanServer.getRegistrationService()).thenReturn(mockRegistrationService); + when(mockLeshanServer.getObservationService()).thenReturn(mockObservationService); + when(mockLeshanServer.getSendService()).thenReturn(mockSendService); + } + + @Test + public void givenRegisterCertificateReloadCallback_whenInvoked_thenShouldRegisterCallback() { + lwm2mTransportService.afterSingletonsInstantiated(); + + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mockConfig).registerServerReloadCallback(callbackCaptor.capture()); + assertThat(callbackCaptor.getValue()).isNotNull(); + } + + @Test + public void givenReloadCallback_whenNewServerCreationFails_thenOldServerIsPreserved() { + lwm2mTransportService.afterSingletonsInstantiated(); + + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mockConfig).registerServerReloadCallback(callbackCaptor.capture()); + Runnable reloadCallback = callbackCaptor.getValue(); + + ReflectionTestUtils.setField(lwm2mTransportService, "server", mockLeshanServer); + + // Force getLhServer() to fail by returning null host (causes InetSocketAddress to throw) + when(mockConfig.getHost()).thenReturn(null); + + // With create-then-swap, the old server should NOT be destroyed if the new one fails. + reloadCallback.run(); + + verify(mockLeshanServer, never()).destroy(); + // Old server should still be the active one + assertThat(ReflectionTestUtils.getField(lwm2mTransportService, "server")).isSameAs(mockLeshanServer); + } + + @Test + public void givenServerWithListeners_whenNewServerCreationFails_thenListenersArePreserved() { + lwm2mTransportService.afterSingletonsInstantiated(); + + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mockConfig).registerServerReloadCallback(callbackCaptor.capture()); + + ReflectionTestUtils.setField(lwm2mTransportService, "server", mockLeshanServer); + + LwM2mServerListener serverListener = new LwM2mServerListener(mockHandler); + ReflectionTestUtils.setField(lwm2mTransportService, "serverListener", serverListener); + + // Invoke the callback — new server creation will fail, old listeners should stay + callbackCaptor.getValue().run(); + + verify(mockRegistrationService, never()).removeListener(any()); + } + + @Test + public void givenMultipleReloadCallbacks_whenInvoked_thenShouldRegisterExactlyOne() { + lwm2mTransportService.afterSingletonsInstantiated(); + + verify(mockConfig, times(1)).registerServerReloadCallback(any()); + } + + @Test + public void givenCertificateReload_whenServerNull_thenShouldNotThrow() { + lwm2mTransportService.afterSingletonsInstantiated(); + + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mockConfig).registerServerReloadCallback(callbackCaptor.capture()); + + ReflectionTestUtils.setField(lwm2mTransportService, "server", null); + + // Should not throw - callback catches exceptions internally + callbackCaptor.getValue().run(); + } + +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java index 1551ef9023..fcb58bc20c 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java @@ -17,6 +17,7 @@ package org.thingsboard.server.transport.mqtt; import io.netty.handler.ssl.SslHandler; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; @@ -48,7 +49,7 @@ import java.util.concurrent.TimeUnit; @Slf4j @Component("MqttSslHandlerProvider") @TbMqttSslTransportComponent -public class MqttSslHandlerProvider { +public class MqttSslHandlerProvider implements SmartInitializingSingleton { @Value("${transport.mqtt.ssl.protocol}") private String sslProtocol; @@ -66,13 +67,29 @@ public class MqttSslHandlerProvider { @Qualifier("mqttSslCredentials") private SslCredentialsConfig mqttSslCredentialsConfig; - private SSLContext sslContext; + private volatile SSLContext sslContext; + + @Override + public void afterSingletonsInstantiated() { + mqttSslCredentialsConfig.registerReloadCallback(() -> { + log.info("MQTT SSL certificates reloaded. Invalidating SSL context..."); + sslContext = null; + log.info("MQTT SSL context invalidated. Will be recreated on next connection."); + }); + } public SslHandler getSslHandler() { - if (sslContext == null) { - sslContext = createSslContext(); + SSLContext ctx = sslContext; + if (ctx == null) { + synchronized (this) { + ctx = sslContext; + if (ctx == null) { + ctx = createSslContext(); + sslContext = ctx; + } + } } - SSLEngine sslEngine = sslContext.createSSLEngine(); + SSLEngine sslEngine = ctx.createSSLEngine(); sslEngine.setUseClientMode(false); sslEngine.setNeedClientAuth(false); sslEngine.setWantClientAuth(true); @@ -98,7 +115,7 @@ public class MqttSslHandlerProvider { sslContext.init(km, tm, null); return sslContext; } catch (Exception e) { - log.error("Unable to set up SSL context. Reason: " + e.getMessage(), e); + log.error("Unable to set up SSL context. Reason: {}", e.getMessage(), e); throw new RuntimeException("Failed to get SSL context", e); } } @@ -106,8 +123,8 @@ public class MqttSslHandlerProvider { private TrustManager getX509TrustManager(TrustManagerFactory tmf) throws Exception { X509TrustManager x509Tm = null; for (TrustManager tm : tmf.getTrustManagers()) { - if (tm instanceof X509TrustManager) { - x509Tm = (X509TrustManager) tm; + if (tm instanceof X509TrustManager x509TrustManager) { + x509Tm = x509TrustManager; break; } } @@ -191,5 +208,7 @@ public class MqttSslHandlerProvider { return false; } } + } + } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java index 3d05e999e0..166a67368c 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java @@ -32,9 +32,6 @@ import org.thingsboard.server.transport.mqtt.gateway.GatewayMetricsService; import java.net.InetSocketAddress; import java.util.concurrent.atomic.AtomicInteger; -/** - * Created by ashvayka on 04.10.18. - */ @Slf4j @Component @TbMqttTransportComponent diff --git a/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProviderTest.java b/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProviderTest.java new file mode 100644 index 0000000000..96183c2934 --- /dev/null +++ b/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProviderTest.java @@ -0,0 +1,197 @@ +/** + * 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.transport.mqtt; + +import io.netty.handler.ssl.SslHandler; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.common.transport.config.ssl.SslCredentials; +import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class MqttSslHandlerProviderTest { + + @Mock + private SslCredentialsConfig mockCredentialsConfig; + + @Mock + private SslCredentials mockCredentials; + + @Mock + private TransportService mockTransportService; + + private MqttSslHandlerProvider sslHandlerProvider; + + @BeforeEach + public void setup() throws Exception { + sslHandlerProvider = new MqttSslHandlerProvider(); + ReflectionTestUtils.setField(sslHandlerProvider, "mqttSslCredentialsConfig", mockCredentialsConfig); + ReflectionTestUtils.setField(sslHandlerProvider, "transportService", mockTransportService); + ReflectionTestUtils.setField(sslHandlerProvider, "sslProtocol", "TLSv1.2"); + + KeyManagerFactory mockKmf = mock(KeyManagerFactory.class); + TrustManagerFactory mockTmf = mock(TrustManagerFactory.class); + X509TrustManager mockTrustManager = mock(X509TrustManager.class); + + when(mockCredentialsConfig.getCredentials()).thenReturn(mockCredentials); + when(mockCredentials.createKeyManagerFactory()).thenReturn(mockKmf); + when(mockCredentials.createTrustManagerFactory()).thenReturn(mockTmf); + when(mockKmf.getKeyManagers()).thenReturn(new KeyManager[0]); + when(mockTmf.getTrustManagers()).thenReturn(new TrustManager[]{mockTrustManager}); + } + + @Test + public void givenInitialized_whenGetSslHandler_thenShouldCreateSSLContext() { + sslHandlerProvider.afterSingletonsInstantiated(); + + SslHandler handler1 = sslHandlerProvider.getSslHandler(); + SslHandler handler2 = sslHandlerProvider.getSslHandler(); + + assertThat(handler1).isNotNull(); + assertThat(handler2).isNotNull(); + assertThat(handler1).isNotSameAs(handler2); + + SSLContext context = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); + assertThat(context).isNotNull(); + } + + @Test + public void givenCertificatesReloaded_whenGetSslHandler_thenShouldRecreateSSLContext() { + sslHandlerProvider.afterSingletonsInstantiated(); + + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); + Runnable reloadCallback = callbackCaptor.getValue(); + + SslHandler handler1 = sslHandlerProvider.getSslHandler(); + SSLContext initialContext = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); + assertThat(initialContext).isNotNull(); + + reloadCallback.run(); + + assertThat(handler1).isNotNull(); + SSLContext contextAfterReload = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); + assertThat(contextAfterReload).isNull(); + + SslHandler handler2 = sslHandlerProvider.getSslHandler(); + SSLContext newContext = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); + + assertThat(handler2).isNotNull(); + assertThat(newContext).isNotNull(); + assertThat(newContext).isNotSameAs(initialContext); + } + + @Test + public void givenConcurrentGetSslHandlerCalls_whenSSLContextNull_thenShouldCreateOnlyOnce() throws Exception { + sslHandlerProvider.afterSingletonsInstantiated(); + + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); + callbackCaptor.getValue().run(); + + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch doneLatch = new CountDownLatch(5); + + for (int i = 0; i < 5; i++) { + new Thread(() -> { + try { + startLatch.await(); + SslHandler handler = sslHandlerProvider.getSslHandler(); + assertThat(handler).isNotNull(); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + doneLatch.countDown(); + } + }).start(); + } + + startLatch.countDown(); + boolean completed = doneLatch.await(5, TimeUnit.SECONDS); + + assertThat(completed).isTrue(); + SSLContext context = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); + assertThat(context).isNotNull(); + } + + @Test + public void givenReloadCallback_whenInvoked_thenShouldInvalidateSSLContext() { + sslHandlerProvider.afterSingletonsInstantiated(); + + sslHandlerProvider.getSslHandler(); + SSLContext initialContext = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); + assertThat(initialContext).isNotNull(); + + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); + + callbackCaptor.getValue().run(); + + SSLContext contextAfterReload = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); + assertThat(contextAfterReload).isNull(); + } + + @Test + public void givenMultipleReloads_whenGetSslHandler_thenShouldRecreateEachTime() { + sslHandlerProvider.afterSingletonsInstantiated(); + + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); + Runnable reloadCallback = callbackCaptor.getValue(); + + SSLContext context1; + SSLContext context2; + SSLContext context3; + + sslHandlerProvider.getSslHandler(); + context1 = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); + assertThat(context1).isNotNull(); + + reloadCallback.run(); + sslHandlerProvider.getSslHandler(); + context2 = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); + assertThat(context2).isNotNull(); + assertThat(context2).isNotSameAs(context1); + + reloadCallback.run(); + sslHandlerProvider.getSslHandler(); + context3 = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); + assertThat(context3).isNotNull(); + assertThat(context3).isNotSameAs(context2); + assertThat(context3).isNotSameAs(context1); + } + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/DeviceDeletedEvent.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/DeviceDeletedEvent.java index df30e2879b..c595e3ca83 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/DeviceDeletedEvent.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/DeviceDeletedEvent.java @@ -19,9 +19,13 @@ import lombok.Getter; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.queue.discovery.event.TbApplicationEvent; +import java.io.Serial; + public final class DeviceDeletedEvent extends TbApplicationEvent { + @Serial private static final long serialVersionUID = -7453664970966733857L; + @Getter private final DeviceId deviceId; @@ -29,4 +33,5 @@ public final class DeviceDeletedEvent extends TbApplicationEvent { super(new Object()); this.deviceId = deviceId; } + } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java index 1e03156ec8..12857e3208 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java @@ -30,9 +30,6 @@ import org.thingsboard.server.gen.transport.TransportProtos.UplinkNotificationMs import java.util.Optional; import java.util.UUID; -/** - * Created by ashvayka on 04.10.18. - */ public interface SessionMsgListener { void onGetAttributesResponse(GetAttributeResponseMsg getAttributesResponse); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java index 9317652719..afd7bd2fed 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java @@ -30,9 +30,6 @@ import org.thingsboard.server.queue.scheduler.SchedulerComponent; import java.util.concurrent.ExecutorService; -/** - * Created by ashvayka on 15.10.18. - */ @Slf4j @Data public abstract class TransportContext { @@ -77,6 +74,4 @@ public abstract class TransportContext { return serviceInfoProvider.getServiceId(); } - - } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java index 5c7d552855..939f2278ce 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java @@ -66,9 +66,6 @@ import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicInteger; -/** - * Created by ashvayka on 04.10.18. - */ public interface TransportService { GetEntityProfileResponseMsg getEntityProfile(GetEntityProfileRequestMsg msg); @@ -162,4 +159,5 @@ public interface TransportService { boolean hasSession(SessionInfoProto sessionInfo); void createGaugeStats(String openConnections, AtomicInteger connectionsCounter); + } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportServiceCallback.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportServiceCallback.java index 41cb57da04..82849b5bdd 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportServiceCallback.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportServiceCallback.java @@ -15,9 +15,6 @@ */ package org.thingsboard.server.common.transport; -/** - * Created by ashvayka on 04.10.18. - */ public interface TransportServiceCallback { TransportServiceCallback EMPTY = new TransportServiceCallback() { diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/AbstractSslCredentials.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/AbstractSslCredentials.java index f15fc42364..9ffa4f7aac 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/AbstractSslCredentials.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/AbstractSslCredentials.java @@ -37,41 +37,56 @@ import java.util.Enumeration; import java.util.HashSet; import java.util.Optional; import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; public abstract class AbstractSslCredentials implements SslCredentials { - private char[] keyPasswordArray; + private record SslState( + char[] keyPasswordArray, + KeyStore keyStore, + PrivateKey privateKey, + PublicKey publicKey, + X509Certificate[] chain, + X509Certificate[] trusts + ) {} - private KeyStore keyStore; - - private PrivateKey privateKey; - - private PublicKey publicKey; - - private X509Certificate[] chain; - - private X509Certificate[] trusts; + private final AtomicReference state = new AtomicReference<>(); @Override public void init(boolean trustsOnly) throws IOException, GeneralSecurityException { + SslState newState = buildState(trustsOnly); + state.set(newState); + } + + @Override + public void reload(boolean trustsOnly) throws IOException, GeneralSecurityException { + SslState newState = buildState(trustsOnly); + state.set(newState); + } + + private SslState buildState(boolean trustsOnly) throws IOException, GeneralSecurityException { String keyPassword = getKeyPassword(); + char[] keyPasswordArray; if (StringUtils.isEmpty(keyPassword)) { - this.keyPasswordArray = new char[0]; + keyPasswordArray = new char[0]; } else { - this.keyPasswordArray = keyPassword.toCharArray(); + keyPasswordArray = keyPassword.toCharArray(); } - this.keyStore = this.loadKeyStore(trustsOnly, this.keyPasswordArray); - Set trustedCerts = getTrustedCerts(this.keyStore, trustsOnly); - this.trusts = trustedCerts.toArray(new X509Certificate[0]); + KeyStore keyStore = this.loadKeyStore(trustsOnly, keyPasswordArray); + Set trustedCerts = getTrustedCerts(keyStore, trustsOnly); + X509Certificate[] trusts = trustedCerts.toArray(new X509Certificate[0]); + PrivateKey privateKey = null; + PublicKey publicKey = null; + X509Certificate[] chain = null; if (!trustsOnly) { PrivateKeyEntry privateKeyEntry = null; String keyAlias = this.getKeyAlias(); if (!StringUtils.isEmpty(keyAlias)) { - privateKeyEntry = tryGetPrivateKeyEntry(this.keyStore, keyAlias, this.keyPasswordArray); + privateKeyEntry = tryGetPrivateKeyEntry(keyStore, keyAlias, keyPasswordArray); } else { - for (Enumeration e = this.keyStore.aliases(); e.hasMoreElements(); ) { + for (Enumeration e = keyStore.aliases(); e.hasMoreElements(); ) { String alias = e.nextElement(); - privateKeyEntry = tryGetPrivateKeyEntry(this.keyStore, alias, this.keyPasswordArray); + privateKeyEntry = tryGetPrivateKeyEntry(keyStore, alias, keyPasswordArray); if (privateKeyEntry != null) { this.updateKeyAlias(alias); break; @@ -82,50 +97,61 @@ public abstract class AbstractSslCredentials implements SslCredentials { throw new IllegalArgumentException("Failed to get private key from the keystore or pem files. " + "Please check if the private key exists in the keystore or pem files and if the provided private key password is valid."); } - this.chain = asX509Certificates(privateKeyEntry.getCertificateChain()); - this.privateKey = privateKeyEntry.getPrivateKey(); - if (this.chain.length > 0) { - this.publicKey = this.chain[0].getPublicKey(); + chain = asX509Certificates(privateKeyEntry.getCertificateChain()); + privateKey = privateKeyEntry.getPrivateKey(); + if (chain.length > 0) { + publicKey = chain[0].getPublicKey(); } } + return new SslState(keyPasswordArray, keyStore, privateKey, publicKey, chain, trusts); + } + + private SslState getState() { + SslState s = state.get(); + if (s == null) { + throw new IllegalStateException("SSL credentials not initialized. Call init() first."); + } + return s; } @Override public KeyStore getKeyStore() { - return this.keyStore; + return getState().keyStore; } @Override public PrivateKey getPrivateKey() { - return this.privateKey; + return getState().privateKey; } @Override public PublicKey getPublicKey() { - return this.publicKey; + return getState().publicKey; } @Override public X509Certificate[] getCertificateChain() { - return this.chain; + return getState().chain; } @Override public X509Certificate[] getTrustedCertificates() { - return this.trusts; + return getState().trusts; } @Override public TrustManagerFactory createTrustManagerFactory() throws NoSuchAlgorithmException, KeyStoreException { + SslState s = getState(); TrustManagerFactory tmFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - tmFactory.init(this.keyStore); + tmFactory.init(s.keyStore); return tmFactory; } @Override public KeyManagerFactory createKeyManagerFactory() throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException { + SslState s = getState(); KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - kmf.init(this.keyStore, this.keyPasswordArray); + kmf.init(s.keyStore, s.keyPasswordArray); return kmf; } @@ -133,7 +159,7 @@ public abstract class AbstractSslCredentials implements SslCredentials { public String getValueFromSubjectNameByKey(String subjectName, String key) { String[] dns = subjectName.split(","); Optional cn = (Arrays.stream(dns).filter(dn -> dn.contains(key + "="))).findFirst(); - String value = cn.isPresent() ? cn.get().replace(key + "=", "") : null; + String value = cn.map(s -> s.replace(key + "=", "")).orElse(null); return StringUtils.isNotEmpty(value) ? value : null; } @@ -189,7 +215,7 @@ public abstract class AbstractSslCredentials implements SslCredentials { if (cert instanceof X509Certificate) { if (trustsOnly) { // is CA certificate - if (((X509Certificate) cert).getBasicConstraints()>=0) { + if (((X509Certificate) cert).getBasicConstraints() >= 0) { set.add((X509Certificate) cert); } } else { @@ -203,12 +229,12 @@ public abstract class AbstractSslCredentials implements SslCredentials { if (trustsOnly) { for (Certificate cert : certs) { // is CA certificate - if (((X509Certificate) cert).getBasicConstraints()>=0) { + if (((X509Certificate) cert).getBasicConstraints() >= 0) { set.add((X509Certificate) cert); } } } else { - set.add((X509Certificate)certs[0]); + set.add((X509Certificate) certs[0]); } } } @@ -216,4 +242,5 @@ public abstract class AbstractSslCredentials implements SslCredentials { } catch (KeyStoreException ignored) {} return Collections.unmodifiableSet(set); } + } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/KeystoreSslCredentials.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/KeystoreSslCredentials.java index 33851f50b1..7a2fb1a545 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/KeystoreSslCredentials.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/KeystoreSslCredentials.java @@ -20,10 +20,14 @@ import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.ResourceUtils; import org.thingsboard.server.common.data.StringUtils; +import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Path; import java.security.GeneralSecurityException; import java.security.KeyStore; +import java.util.Collections; +import java.util.List; @Data @EqualsAndHashCode(callSuper = true) @@ -54,4 +58,16 @@ public class KeystoreSslCredentials extends AbstractSslCredentials { protected void updateKeyAlias(String keyAlias) { this.keyAlias = keyAlias; } + + @Override + public List getCertificateFilePaths() { + if (!StringUtils.isEmpty(storeFile) && !storeFile.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) { + File storeFileObj = new File(storeFile); + if (storeFileObj.exists()) { + return Collections.singletonList(storeFileObj.toPath().toAbsolutePath()); + } + } + return Collections.emptyList(); + } + } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/PemSslCredentials.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/PemSslCredentials.java index cb2c9ba97b..72ad7af9c5 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/PemSslCredentials.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/PemSslCredentials.java @@ -30,9 +30,11 @@ import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; import org.thingsboard.server.common.data.ResourceUtils; import org.thingsboard.server.common.data.StringUtils; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.nio.file.Path; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.PrivateKey; @@ -76,13 +78,13 @@ public class PemSslCredentials extends AbstractSslCredentials { if (object instanceof X509CertificateHolder) { X509Certificate x509Cert = certConverter.getCertificate((X509CertificateHolder) object); certificates.add(x509Cert); - } else if (object instanceof PEMEncryptedKeyPair) { + } else if (object instanceof PEMEncryptedKeyPair pemEncryptedKeyPair) { PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(keyPasswordArray); - privateKey = keyConverter.getKeyPair(((PEMEncryptedKeyPair) object).decryptKeyPair(decProv)).getPrivate(); - } else if (object instanceof PEMKeyPair) { - privateKey = keyConverter.getKeyPair((PEMKeyPair) object).getPrivate(); - } else if (object instanceof PrivateKeyInfo) { - privateKey = keyConverter.getPrivateKey((PrivateKeyInfo) object); + privateKey = keyConverter.getKeyPair(pemEncryptedKeyPair.decryptKeyPair(decProv)).getPrivate(); + } else if (object instanceof PEMKeyPair pemKeyPair) { + privateKey = keyConverter.getKeyPair(pemKeyPair).getPrivate(); + } else if (object instanceof PrivateKeyInfo privateKeyInfo) { + privateKey = keyConverter.getPrivateKey(privateKeyInfo); } } } @@ -93,15 +95,15 @@ public class PemSslCredentials extends AbstractSslCredentials { try (PEMParser pemParser = new PEMParser(new InputStreamReader(inStream))) { Object object; while ((object = pemParser.readObject()) != null) { - if (object instanceof PEMEncryptedKeyPair) { + if (object instanceof PEMEncryptedKeyPair pemEncryptedKeyPair) { PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(keyPasswordArray); - privateKey = keyConverter.getKeyPair(((PEMEncryptedKeyPair) object).decryptKeyPair(decProv)).getPrivate(); + privateKey = keyConverter.getKeyPair(pemEncryptedKeyPair.decryptKeyPair(decProv)).getPrivate(); break; - } else if (object instanceof PEMKeyPair) { - privateKey = keyConverter.getKeyPair((PEMKeyPair) object).getPrivate(); + } else if (object instanceof PEMKeyPair pemKeyPair) { + privateKey = keyConverter.getKeyPair(pemKeyPair).getPrivate(); break; - } else if (object instanceof PrivateKeyInfo) { - privateKey = keyConverter.getPrivateKey((PrivateKeyInfo) object); + } else if (object instanceof PrivateKeyInfo privateKeyInfo) { + privateKey = keyConverter.getPrivateKey(privateKeyInfo); } } } @@ -138,6 +140,27 @@ public class PemSslCredentials extends AbstractSslCredentials { } @Override - protected void updateKeyAlias(String keyAlias) { + protected void updateKeyAlias(String keyAlias) {} + + @Override + public List getCertificateFilePaths() { + List paths = new ArrayList<>(); + + if (!StringUtils.isEmpty(certFile) && !certFile.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) { + File certFileObj = new File(certFile); + if (certFileObj.exists()) { + paths.add(certFileObj.toPath().toAbsolutePath()); + } + } + + if (!StringUtils.isEmpty(keyFile) && !keyFile.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) { + File keyFileObj = new File(keyFile); + if (keyFileObj.exists()) { + paths.add(keyFileObj.toPath().toAbsolutePath()); + } + } + + return paths; } + } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentials.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentials.java index 89d4540b77..89dccac18e 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentials.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentials.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.transport.config.ssl; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.TrustManagerFactory; import java.io.IOException; +import java.nio.file.Path; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.KeyStoreException; @@ -26,11 +27,14 @@ import java.security.PrivateKey; import java.security.PublicKey; import java.security.UnrecoverableKeyException; import java.security.cert.X509Certificate; +import java.util.List; public interface SslCredentials { void init(boolean trustsOnly) throws IOException, GeneralSecurityException; + void reload(boolean trustsOnly) throws IOException, GeneralSecurityException; + KeyStore getKeyStore(); String getKeyPassword(); @@ -50,4 +54,7 @@ public interface SslCredentials { KeyManagerFactory createKeyManagerFactory() throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException; String getValueFromSubjectNameByKey(String subjectName, String key); + + List getCertificateFilePaths(); + } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsConfig.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsConfig.java index 0df22a33cb..3646c4f37d 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsConfig.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsConfig.java @@ -19,6 +19,9 @@ import jakarta.annotation.PostConstruct; import lombok.Data; import lombok.extern.slf4j.Slf4j; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + @Slf4j @Data public class SslCredentialsConfig { @@ -33,6 +36,8 @@ public class SslCredentialsConfig { private final String name; private final boolean trustsOnly; + private final List reloadCallbacks = new CopyOnWriteArrayList<>(); + public SslCredentialsConfig(String name, boolean trustsOnly) { this.name = name; this.trustsOnly = trustsOnly; @@ -62,4 +67,26 @@ public class SslCredentialsConfig { } } + public void onCertificateFileChanged() { + try { + log.info("{}: Certificate file changed. Reloading SSL credentials...", name); + this.credentials.reload(this.trustsOnly); + log.info("{}: SSL credentials reloaded successfully.", name); + + for (Runnable callback : reloadCallbacks) { + try { + callback.run(); + } catch (Exception e) { + log.error("{}: Error executing reload callback", name, e); + } + } + } catch (Exception e) { + log.error("{}: Failed to reload SSL credentials", name, e); + } + } + + public void registerReloadCallback(Runnable callback) { + this.reloadCallbacks.add(callback); + } + } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsWebServerCustomizer.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsWebServerCustomizer.java index 34cc1151c4..147daefa7e 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsWebServerCustomizer.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsWebServerCustomizer.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.transport.config.ssl; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; @@ -30,71 +32,124 @@ import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.BiConsumer; import java.util.function.Consumer; +@Slf4j @Component @ConditionalOnExpression("'${spring.main.web-environment:true}'=='true' && '${server.ssl.enabled:false}'=='true'") -public class SslCredentialsWebServerCustomizer implements WebServerFactoryCustomizer { +public class SslCredentialsWebServerCustomizer implements WebServerFactoryCustomizer, SmartInitializingSingleton { - @Bean - @ConfigurationProperties(prefix = "server.ssl.credentials") - public SslCredentialsConfig httpServerSslCredentials() { - return new SslCredentialsConfig("HTTP Server SSL Credentials", false); - } + private static final String DEFAULT_BUNDLE_NAME = "default"; + + private final ServerProperties serverProperties; + private final List> updateHandlers = new CopyOnWriteArrayList<>(); @Autowired @Qualifier("httpServerSslCredentials") private SslCredentialsConfig httpServerSslCredentialsConfig; @Autowired - SslBundles sslBundles; - - private final ServerProperties serverProperties; + private SslBundles sslBundles; public SslCredentialsWebServerCustomizer(ServerProperties serverProperties) { this.serverProperties = serverProperties; } + @Bean + @ConfigurationProperties(prefix = "server.ssl.credentials") + public SslCredentialsConfig httpServerSslCredentials() { + return new SslCredentialsConfig("HTTP Server SSL Credentials", false); + } + + @Bean + public SslBundles sslBundles() { + return new DynamicSslBundles(); + } + @Override public void customize(ConfigurableServletWebServerFactory factory) { - SslCredentials sslCredentials = this.httpServerSslCredentialsConfig.getCredentials(); + SslCredentials credentials = httpServerSslCredentialsConfig.getCredentials(); + Ssl ssl = serverProperties.getSsl(); - ssl.setBundle("default"); - ssl.setKeyAlias(sslCredentials.getKeyAlias()); - ssl.setKeyPassword(sslCredentials.getKeyPassword()); + ssl.setBundle(DEFAULT_BUNDLE_NAME); + ssl.setKeyAlias(credentials.getKeyAlias()); + ssl.setKeyPassword(credentials.getKeyPassword()); + factory.setSsl(ssl); factory.setSslBundles(sslBundles); } - @Bean - public SslBundles sslBundles() { + @Override + public void afterSingletonsInstantiated() { + httpServerSslCredentialsConfig.registerReloadCallback(this::reloadSslCertificates); + } + + private void reloadSslCertificates() { + try { + log.info("Reloading HTTP Server SSL certificates..."); + + SslBundle newBundle = createSslBundle(); + notifyUpdateHandlers(newBundle); + + log.info("HTTP Server SSL certificates reloaded successfully"); + } catch (Exception e) { + log.error("Failed to reload HTTP Server SSL certificates", e); + } + } + + private SslBundle createSslBundle() { + SslCredentials credentials = httpServerSslCredentialsConfig.getCredentials(); + SslStoreBundle storeBundle = SslStoreBundle.of( - httpServerSslCredentialsConfig.getCredentials().getKeyStore(), - httpServerSslCredentialsConfig.getCredentials().getKeyPassword(), + credentials.getKeyStore(), + credentials.getKeyPassword(), null ); - return new SslBundles() { - @Override - public SslBundle getBundle(String name) { - return SslBundle.of(storeBundle); - } + return SslBundle.of(storeBundle); + } - @Override - public List getBundleNames() { - return List.of("default"); + private void notifyUpdateHandlers(SslBundle newBundle) { + for (Consumer handler : updateHandlers) { + try { + handler.accept(newBundle); + } catch (Exception e) { + log.error("Failed to notify SSL bundle update handler", e); } + } + } + + private class DynamicSslBundles implements SslBundles { - @Override - public void addBundleUpdateHandler(String name, Consumer handler) { - // no-op + @Override + public SslBundle getBundle(String name) { + if (!DEFAULT_BUNDLE_NAME.equals(name)) { + throw new IllegalArgumentException("Unknown SSL bundle: " + name); } + return createSslBundle(); + } - @Override - public void addBundleRegisterHandler(BiConsumer handler) { - // no-op + @Override + public List getBundleNames() { + return List.of(DEFAULT_BUNDLE_NAME); + } + + @Override + public void addBundleUpdateHandler(String name, Consumer handler) { + if (DEFAULT_BUNDLE_NAME.equals(name)) { + updateHandlers.add(handler); + log.debug("Registered SSL bundle update handler for bundle: {}", name); + } else { + log.warn("Attempted to register update handler for unknown bundle: {}", name); } - }; + } + + @Override + public void addBundleRegisterHandler(BiConsumer registerHandler) { + + } + } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java new file mode 100644 index 0000000000..19fc545d0b --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java @@ -0,0 +1,272 @@ +/** + * 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.common.transport.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.transport.config.ssl.SslCredentials; +import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig; +import org.thingsboard.server.queue.util.TbTransportComponent; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +@TbTransportComponent +public class CertificateReloadManager implements SmartInitializingSingleton, DisposableBean { + + private static final int MAX_CONSECUTIVE_FAILURES = 10; + + @Value("${transport.ssl.certificate.reload.enabled:true}") + private boolean reloadEnabled; + + @Value("${transport.ssl.certificate.reload.check_interval:60}") + private long checkIntervalInSeconds; + + @Autowired + protected ApplicationContext applicationContext; + + private final Map watchers = new ConcurrentHashMap<>(); + private volatile ScheduledExecutorService scheduler; + + public void registerWatcher(String name, Path certPath, Runnable reloadCallback) { + registerWatcher(name, List.of(certPath), reloadCallback); + } + + public void registerWatcher(String name, List certPaths, Runnable reloadCallback) { + watchers.put(name, new CertificateWatcher(certPaths, reloadCallback)); + log.info("Registered certificate watcher for: {} (watching {} file(s))", name, certPaths.size()); + } + + private void checkCertificates() { + watchers.forEach((name, watcher) -> { + try { + watcher.checkAndReload(name); + } catch (Exception e) { + log.error("Error checking certificate for {}: {}", name, e.getMessage(), e); + } + }); + } + + private void discoverAndRegisterSslCredentials() { + try { + Map sslConfigBeans = applicationContext.getBeansOfType(SslCredentialsConfig.class); + + log.info("Found {} SslCredentialsConfig beans", sslConfigBeans.size()); + + for (Map.Entry entry : sslConfigBeans.entrySet()) { + String beanName = entry.getKey(); + SslCredentialsConfig config = entry.getValue(); + + try { + if (!config.isEnabled()) { + log.debug("Skipping disabled SSL config: {} ({})", config.getName(), beanName); + continue; + } + + SslCredentials credentials = config.getCredentials(); + if (credentials == null) { + log.debug("Skipping uninitialized SSL config: {} ({})", config.getName(), beanName); + continue; + } + + List filePaths = credentials.getCertificateFilePaths(); + if (filePaths == null || filePaths.isEmpty()) { + log.debug("No certificate files to watch for: {} ({})", config.getName(), beanName); + continue; + } + + List existingPaths = filePaths.stream() + .filter(p -> p != null && Files.exists(p)) + .toList(); + + for (Path filePath : filePaths) { + if (filePath == null || !Files.exists(filePath)) { + log.warn("Certificate file does not exist: {} (from {})", filePath, config.getName()); + } + } + + if (!existingPaths.isEmpty()) { + registerWatcher(config.getName(), existingPaths, config::onCertificateFileChanged); + log.info("Registered certificate watcher: {} -> {}", config.getName(), existingPaths); + } + + } catch (Exception e) { + log.error("Error registering watchers for SSL config: {} ({})", config.getName(), beanName, e); + } + } + + } catch (Exception e) { + log.error("Error discovering SSL credentials configs", e); + } + } + + @Override + public void destroy() throws Exception { + if (scheduler != null) { + scheduler.shutdown(); + if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { + scheduler.shutdownNow(); + } + } + } + + @Override + public void afterSingletonsInstantiated() { + if (!reloadEnabled) { + log.trace("Auto-reload of certificates is disabled. Skipping initialization..."); + return; + } + log.info("Initializing Certificate Reload Manager..."); + + discoverAndRegisterSslCredentials(); + + scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("certificate-reload-manager")); + scheduler.scheduleWithFixedDelay(this::checkCertificates, checkIntervalInSeconds, checkIntervalInSeconds, TimeUnit.SECONDS); + } + + static class CertificateWatcher { + private final List paths; + private final Runnable reloadCallback; + private final Map lastModifiedMap; + private final Map lastChecksumMap; + private int consecutiveFailures; + private String failedCombinedChecksum; + + CertificateWatcher(List paths, Runnable reloadCallback) { + this.paths = paths; + this.reloadCallback = reloadCallback; + this.lastModifiedMap = new HashMap<>(); + this.lastChecksumMap = new HashMap<>(); + for (Path path : paths) { + lastModifiedMap.put(path, getLastModifiedTime(path)); + lastChecksumMap.put(path, calculateChecksum(path)); + } + this.consecutiveFailures = 0; + } + + synchronized void checkAndReload(String name) { + boolean anyModifiedChanged = false; + for (Path path : paths) { + long currentModified = getLastModifiedTime(path); + Long lastModified = lastModifiedMap.getOrDefault(path, 0L); + if (currentModified != lastModified) { + anyModifiedChanged = true; + break; + } + } + if (!anyModifiedChanged) { + return; + } + + // Compute combined checksum of all files + Map currentChecksums = new HashMap<>(); + StringBuilder combined = new StringBuilder(); + for (Path path : paths) { + String checksum = calculateChecksum(path); + currentChecksums.put(path, checksum); + combined.append(checksum); + } + String combinedChecksum = combined.toString(); + + // Build old combined checksum for comparison + StringBuilder oldCombined = new StringBuilder(); + for (Path path : paths) { + oldCombined.append(lastChecksumMap.getOrDefault(path, "")); + } + String oldCombinedChecksum = oldCombined.toString(); + + if (combinedChecksum.equals(oldCombinedChecksum)) { + // Content unchanged, just update modification times + for (Path path : paths) { + lastModifiedMap.put(path, getLastModifiedTime(path)); + } + return; + } + + if (!combinedChecksum.equals(failedCombinedChecksum) && consecutiveFailures > 0) { + // File content changed since last failure — reset and retry + consecutiveFailures = 0; + failedCombinedChecksum = null; + } + + if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) { + return; + } + + try { + log.info("Certificate change detected for: {}. Triggering reload...", name); + reloadCallback.run(); + for (Path path : paths) { + lastModifiedMap.put(path, getLastModifiedTime(path)); + lastChecksumMap.put(path, currentChecksums.get(path)); + } + consecutiveFailures = 0; + failedCombinedChecksum = null; + } catch (Exception e) { + consecutiveFailures++; + failedCombinedChecksum = combinedChecksum; + log.error("Failed to reload certificate for {} (attempt {}/{}): {}", + name, consecutiveFailures, MAX_CONSECUTIVE_FAILURES, e.getMessage(), e); + } + } + + private long getLastModifiedTime(Path path) { + try { + if (!Files.exists(path)) { + return 0; + } + return Files.getLastModifiedTime(path).toMillis(); + } catch (IOException e) { + return 0; + } + } + + private String calculateChecksum(Path path) { + try { + if (!Files.exists(path)) { + return ""; + } + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] bytes = Files.readAllBytes(path); + byte[] hash = md.digest(bytes); + return Base64.getEncoder().encodeToString(hash); + } catch (Exception e) { + log.warn("Failed to calculate checksum for certificate file: {}", path, e); + return ""; + } + } + + } + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 260957c990..240de91424 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -127,9 +127,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; -/** - * Created by ashvayka on 17.10.18. - */ @Slf4j @Service @TbTransportComponent @@ -789,7 +786,7 @@ public class DefaultTransportService extends TransportActivityManager implements TransportProtos.SessionCloseNotificationProto notification = TransportProtos.SessionCloseNotificationProto.newBuilder().setMessage("session timeout!").build(); - ScheduledFuture executorFuture = scheduler.schedule(() -> { + ScheduledFuture executorFuture = scheduler.schedule(() -> { listener.onRemoteSessionCloseCommand(sessionId, notification); deregisterSession(sessionInfo); }, timeout, TimeUnit.MILLISECONDS); @@ -1169,6 +1166,7 @@ public class DefaultTransportService extends TransportActivityManager implements public void onFailure(Throwable t) { DefaultTransportService.this.transportCallbackExecutor.submit(() -> callback.onError(t)); } + } private static class StatsCallback implements TbQueueCallback { @@ -1183,16 +1181,19 @@ public class DefaultTransportService extends TransportActivityManager implements @Override public void onSuccess(TbQueueMsgMetadata metadata) { stats.incrementSuccessful(); - if (callback != null) + if (callback != null) { callback.onSuccess(metadata); + } } @Override public void onFailure(Throwable t) { stats.incrementFailed(); - if (callback != null) + if (callback != null) { callback.onFailure(t); + } } + } private class MsgPackCallback implements TbQueueCallback { @@ -1215,6 +1216,7 @@ public class DefaultTransportService extends TransportActivityManager implements public void onFailure(Throwable t) { DefaultTransportService.this.transportCallbackExecutor.submit(() -> callback.onError(t)); } + } private class ApiStatsProxyCallback implements TransportServiceCallback { @@ -1244,6 +1246,7 @@ public class DefaultTransportService extends TransportActivityManager implements public void onError(Throwable e) { callback.onError(e); } + } @Override @@ -1270,4 +1273,5 @@ public class DefaultTransportService extends TransportActivityManager implements log.info("Transport Stats: {}", values); } } + } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/SessionMetaData.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/SessionMetaData.java index 245f167ef8..612871f22a 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/SessionMetaData.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/SessionMetaData.java @@ -21,9 +21,6 @@ import org.thingsboard.server.gen.transport.TransportProtos; import java.util.concurrent.ScheduledFuture; -/** - * Created by ashvayka on 15.10.18. - */ @Data public class SessionMetaData { @@ -47,11 +44,8 @@ public class SessionMetaData { this.scheduledFuture = scheduledFuture; } - public ScheduledFuture getScheduledFuture() { - return scheduledFuture; - } - public boolean hasScheduledFuture() { return null != this.scheduledFuture; } + } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/ToRuleEngineMsgEncoder.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/ToRuleEngineMsgEncoder.java index a353cf94e4..fd144e0110 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/ToRuleEngineMsgEncoder.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/ToRuleEngineMsgEncoder.java @@ -18,9 +18,6 @@ package org.thingsboard.server.common.transport.service; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.queue.kafka.TbKafkaEncoder; -/** - * Created by ashvayka on 05.10.18. - */ public class ToRuleEngineMsgEncoder implements TbKafkaEncoder { @Override public byte[] encode(ToRuleEngineMsg value) { diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/ToTransportMsgResponseDecoder.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/ToTransportMsgResponseDecoder.java index 2e1d292a63..13a99686ad 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/ToTransportMsgResponseDecoder.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/ToTransportMsgResponseDecoder.java @@ -21,9 +21,6 @@ import org.thingsboard.server.queue.kafka.TbKafkaDecoder; import java.io.IOException; -/** - * Created by ashvayka on 05.10.18. - */ public class ToTransportMsgResponseDecoder implements TbKafkaDecoder { @Override diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportApiRequestEncoder.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportApiRequestEncoder.java index 2de10a70fd..4a907c836a 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportApiRequestEncoder.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportApiRequestEncoder.java @@ -18,9 +18,6 @@ package org.thingsboard.server.common.transport.service; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.queue.kafka.TbKafkaEncoder; -/** - * Created by ashvayka on 05.10.18. - */ public class TransportApiRequestEncoder implements TbKafkaEncoder { @Override public byte[] encode(TransportApiRequestMsg value) { diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportApiResponseDecoder.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportApiResponseDecoder.java index cfb7168e66..563d1078c9 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportApiResponseDecoder.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportApiResponseDecoder.java @@ -21,9 +21,6 @@ import org.thingsboard.server.queue.kafka.TbKafkaDecoder; import java.io.IOException; -/** - * Created by ashvayka on 05.10.18. - */ public class TransportApiResponseDecoder implements TbKafkaDecoder { @Override diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java index cd0efe2210..535baf921d 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java @@ -30,9 +30,6 @@ import org.thingsboard.server.gen.transport.TransportProtos; import java.util.Optional; import java.util.UUID; -/** - * @author Andrew Shvayka - */ @Data public abstract class DeviceAwareSessionContext implements SessionContext { diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/SessionContext.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/SessionContext.java index ee0786aeb1..df28bb3390 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/SessionContext.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/SessionContext.java @@ -31,4 +31,5 @@ public interface SessionContext { void onDeviceProfileUpdate(TransportProtos.SessionInfoProto sessionInfo, DeviceProfile deviceProfile); void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, Optional deviceProfileOpt); + } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/JsonUtils.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/JsonUtils.java index ecdfc479cb..4d5451d4ca 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/JsonUtils.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/JsonUtils.java @@ -27,8 +27,7 @@ import java.util.regex.Pattern; public class JsonUtils { - private static final Pattern BASE64_PATTERN = - Pattern.compile("^[A-Za-z0-9+/]+={0,2}$"); + private static final Pattern BASE64_PATTERN = Pattern.compile("^[A-Za-z0-9+/]+={0,2}$"); public static JsonObject getJsonObject(List tsKv) { JsonObject json = new JsonObject(); @@ -68,12 +67,12 @@ public class JsonUtils { } return JsonParser.parseString((String) value); } - } else if (value instanceof Boolean) { - return new JsonPrimitive((Boolean) value); - } else if (value instanceof Double) { - return new JsonPrimitive((Double) value); - } else if (value instanceof Float) { - return new JsonPrimitive((Float) value); + } else if (value instanceof Boolean booleanValue) { + return new JsonPrimitive(booleanValue); + } else if (value instanceof Double doubleValue) { + return new JsonPrimitive(doubleValue); + } else if (value instanceof Float floatValue) { + return new JsonPrimitive(floatValue); } else { throw new IllegalArgumentException("Unsupported type: " + value.getClass().getSimpleName()); } @@ -91,4 +90,5 @@ public class JsonUtils { public static boolean isBase64(String value) { return value.length() % 4 == 0 && BASE64_PATTERN.matcher(value).matches(); } + } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java index 30598925d5..0158e23d93 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/SslUtil.java @@ -31,10 +31,6 @@ import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.Base64; - -/** - * @author Valerii Sosliuk - */ @Slf4j public class SslUtil { @@ -51,7 +47,7 @@ public class SslUtil { String begin = "-----BEGIN CERTIFICATE-----"; String end = "-----END CERTIFICATE-----"; StringBuilder stringBuilder = new StringBuilder(); - for (Certificate cert: chain) { + for (Certificate cert : chain) { stringBuilder.append(begin).append(EncryptionUtil.certTrimNewLines(Base64.getEncoder().encodeToString(cert.getEncoded()))).append(end).append("\n"); } return stringBuilder.toString(); @@ -85,4 +81,5 @@ public class SslUtil { RDN cn = x500name.getRDNs(BCStyle.CN)[0]; return IETFUtils.valueToString(cn.getFirst().getValue()); } + } diff --git a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsConfigTest.java b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsConfigTest.java new file mode 100644 index 0000000000..6e16b2dc83 --- /dev/null +++ b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsConfigTest.java @@ -0,0 +1,182 @@ +/** + * 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.common.transport.config.ssl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +public class SslCredentialsConfigTest { + + @Mock + private SslCredentials mockCredentials; + + private SslCredentialsConfig config; + + @BeforeEach + public void setup() { + config = new SslCredentialsConfig("Test SSL Config", false); + } + + @Test + public void givenConfig_whenCreated_thenShouldHaveCorrectName() { + assertThat(config.getName()).isEqualTo("Test SSL Config"); + assertThat(config.isTrustsOnly()).isFalse(); + } + + @Test + public void givenTrustsOnlyConfig_whenCreated_thenShouldHaveCorrectTrustsOnly() { + SslCredentialsConfig trustsOnlyConfig = new SslCredentialsConfig("Trust Config", true); + assertThat(trustsOnlyConfig.isTrustsOnly()).isTrue(); + } + + @Test + public void givenCallback_whenRegistered_thenShouldBeStoredInList() { + AtomicInteger callCount = new AtomicInteger(0); + + config.registerReloadCallback(callCount::incrementAndGet); + config.setCredentials(mockCredentials); + + try { + doNothing().when(mockCredentials).reload(false); + } catch (Exception e) { + throw new RuntimeException(e); + } + + config.onCertificateFileChanged(); + + assertThat(callCount.get()).isEqualTo(1); + } + + @Test + public void givenMultipleCallbacks_whenCertificateChanged_thenAllShouldBeCalled() throws Exception { + AtomicInteger callback1Count = new AtomicInteger(0); + AtomicInteger callback2Count = new AtomicInteger(0); + AtomicInteger callback3Count = new AtomicInteger(0); + + config.registerReloadCallback(callback1Count::incrementAndGet); + config.registerReloadCallback(callback2Count::incrementAndGet); + config.registerReloadCallback(callback3Count::incrementAndGet); + + config.setCredentials(mockCredentials); + doNothing().when(mockCredentials).reload(false); + + config.onCertificateFileChanged(); + + assertThat(callback1Count.get()).isEqualTo(1); + assertThat(callback2Count.get()).isEqualTo(1); + assertThat(callback3Count.get()).isEqualTo(1); + } + + @Test + public void givenCallbackThrowsException_whenCertificateChanged_thenOtherCallbacksShouldStillBeCalled() throws Exception { + AtomicInteger callback1Count = new AtomicInteger(0); + AtomicInteger callback2Count = new AtomicInteger(0); + + config.registerReloadCallback(() -> { + callback1Count.incrementAndGet(); + throw new RuntimeException("Simulated callback failure"); + }); + config.registerReloadCallback(callback2Count::incrementAndGet); + + config.setCredentials(mockCredentials); + doNothing().when(mockCredentials).reload(false); + + config.onCertificateFileChanged(); + + assertThat(callback1Count.get()).isEqualTo(1); + assertThat(callback2Count.get()).isEqualTo(1); + } + + @Test + public void givenCredentialsReloadFails_whenCertificateChanged_thenCallbacksShouldNotBeCalled() throws Exception { + AtomicInteger callbackCount = new AtomicInteger(0); + + config.registerReloadCallback(callbackCount::incrementAndGet); + config.setCredentials(mockCredentials); + + doThrow(new RuntimeException("Simulated reload failure")).when(mockCredentials).reload(false); + + config.onCertificateFileChanged(); + + assertThat(callbackCount.get()).isEqualTo(0); + } + + @Test + public void givenCertificateChanged_whenCredentialsReloadSucceeds_thenShouldCallReload() throws Exception { + config.setCredentials(mockCredentials); + doNothing().when(mockCredentials).reload(false); + + config.onCertificateFileChanged(); + + verify(mockCredentials).reload(false); + } + + @Test + public void givenTrustsOnlyConfig_whenCertificateChanged_thenShouldReloadWithTrustsOnlyTrue() throws Exception { + SslCredentialsConfig trustsOnlyConfig = new SslCredentialsConfig("Trust Config", true); + trustsOnlyConfig.setCredentials(mockCredentials); + doNothing().when(mockCredentials).reload(true); + + trustsOnlyConfig.onCertificateFileChanged(); + + verify(mockCredentials).reload(true); + } + + @Test + public void givenConcurrentCallbackRegistrations_whenCertificateChanged_thenShouldHandleSafely() throws Exception { + AtomicInteger totalCallbacks = new AtomicInteger(0); + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch doneLatch = new CountDownLatch(10); + + for (int i = 0; i < 10; i++) { + new Thread(() -> { + try { + startLatch.await(); + config.registerReloadCallback(totalCallbacks::incrementAndGet); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + doneLatch.countDown(); + } + }).start(); + } + + startLatch.countDown(); + boolean completed = doneLatch.await(5, TimeUnit.SECONDS); + assertThat(completed).isTrue(); + + config.setCredentials(mockCredentials); + doNothing().when(mockCredentials).reload(false); + + config.onCertificateFileChanged(); + + assertThat(totalCallbacks.get()).isEqualTo(10); + } + +} diff --git a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsWebServerCustomizerTest.java b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsWebServerCustomizerTest.java new file mode 100644 index 0000000000..b03e1f2edc --- /dev/null +++ b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsWebServerCustomizerTest.java @@ -0,0 +1,277 @@ +/** + * 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.common.transport.config.ssl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.test.util.ReflectionTestUtils; + +import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class SslCredentialsWebServerCustomizerTest { + + @Mock + private ServerProperties mockServerProperties; + + @Mock + private SslCredentialsConfig mockCredentialsConfig; + + @Mock + private SslCredentials mockCredentials; + + @Mock + private KeyStore mockKeyStore; + + private SslCredentialsWebServerCustomizer customizer; + + @BeforeEach + public void setup() throws Exception { + customizer = new SslCredentialsWebServerCustomizer(mockServerProperties); + ReflectionTestUtils.setField(customizer, "httpServerSslCredentialsConfig", mockCredentialsConfig); + + when(mockCredentialsConfig.getCredentials()).thenReturn(mockCredentials); + when(mockCredentials.getKeyStore()).thenReturn(mockKeyStore); + when(mockCredentials.getKeyPassword()).thenReturn("password"); + when(mockCredentials.getKeyAlias()).thenReturn("server"); + + X509Certificate mockCert = mock(X509Certificate.class); + when(mockCert.getEncoded()).thenReturn("TEST_CERT_DATA".getBytes()); + when(mockCredentials.getCertificateChain()).thenReturn(new X509Certificate[]{mockCert}); + } + + @Test + public void givenInitialized_whenAfterSingletonsInstantiated_thenShouldRegisterReloadCallback() { + customizer.afterSingletonsInstantiated(); + + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); + assertThat(callbackCaptor.getValue()).isNotNull(); + } + + @Test + public void givenReloadCallback_whenInvoked_thenShouldReloadCertificates() { + customizer.afterSingletonsInstantiated(); + + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); + Runnable reloadCallback = callbackCaptor.getValue(); + + reloadCallback.run(); + + verify(mockCredentialsConfig, times(1)).getCredentials(); + } + + @Test + public void givenSslBundles_whenGetBundle_thenShouldReturnValidBundle() { + SslBundles sslBundles = customizer.sslBundles(); + + SslBundle bundle = sslBundles.getBundle("default"); + + assertThat(bundle).isNotNull(); + } + + @Test + public void givenSslBundles_whenGetBundleNames_thenShouldReturnDefault() { + SslBundles sslBundles = customizer.sslBundles(); + + List bundleNames = sslBundles.getBundleNames(); + + assertThat(bundleNames).containsExactly("default"); + } + + @Test + public void givenSslBundles_whenAddUpdateHandler_thenShouldRegisterHandler() { + SslBundles sslBundles = customizer.sslBundles(); + AtomicInteger handlerCallCount = new AtomicInteger(0); + Consumer handler = bundle -> handlerCallCount.incrementAndGet(); + + sslBundles.addBundleUpdateHandler("default", handler); + + customizer.afterSingletonsInstantiated(); + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); + callbackCaptor.getValue().run(); + + assertThat(handlerCallCount.get()).isEqualTo(1); + } + + @Test + public void givenSslBundles_whenAddUpdateHandlerForWrongBundle_thenShouldNotRegister() { + SslBundles sslBundles = customizer.sslBundles(); + AtomicInteger handlerCallCount = new AtomicInteger(0); + Consumer handler = bundle -> handlerCallCount.incrementAndGet(); + + sslBundles.addBundleUpdateHandler("wrong-bundle", handler); + + customizer.afterSingletonsInstantiated(); + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); + callbackCaptor.getValue().run(); + + assertThat(handlerCallCount.get()).isEqualTo(0); + } + + @Test + public void givenMultipleUpdateHandlers_whenReload_thenShouldNotifyAll() { + SslBundles sslBundles = customizer.sslBundles(); + AtomicInteger handler1CallCount = new AtomicInteger(0); + AtomicInteger handler2CallCount = new AtomicInteger(0); + AtomicInteger handler3CallCount = new AtomicInteger(0); + + sslBundles.addBundleUpdateHandler("default", bundle -> handler1CallCount.incrementAndGet()); + sslBundles.addBundleUpdateHandler("default", bundle -> handler2CallCount.incrementAndGet()); + sslBundles.addBundleUpdateHandler("default", bundle -> handler3CallCount.incrementAndGet()); + + customizer.afterSingletonsInstantiated(); + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); + + callbackCaptor.getValue().run(); + + assertThat(handler1CallCount.get()).isEqualTo(1); + assertThat(handler2CallCount.get()).isEqualTo(1); + assertThat(handler3CallCount.get()).isEqualTo(1); + } + + @Test + public void givenMultipleReloads_whenTriggered_thenShouldNotifyHandlersEachTime() { + SslBundles sslBundles = customizer.sslBundles(); + AtomicInteger handlerCallCount = new AtomicInteger(0); + sslBundles.addBundleUpdateHandler("default", bundle -> handlerCallCount.incrementAndGet()); + + customizer.afterSingletonsInstantiated(); + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); + Runnable reloadCallback = callbackCaptor.getValue(); + + reloadCallback.run(); + reloadCallback.run(); + reloadCallback.run(); + + assertThat(handlerCallCount.get()).isEqualTo(3); + } + + @Test + public void givenUpdateHandlerThrowsException_whenReload_thenShouldContinueNotifyingOtherHandlers() { + SslBundles sslBundles = customizer.sslBundles(); + AtomicInteger handler1CallCount = new AtomicInteger(0); + AtomicInteger handler2CallCount = new AtomicInteger(0); + + sslBundles.addBundleUpdateHandler("default", bundle -> { + handler1CallCount.incrementAndGet(); + throw new RuntimeException("Handler 1 failed"); + }); + sslBundles.addBundleUpdateHandler("default", bundle -> handler2CallCount.incrementAndGet()); + + customizer.afterSingletonsInstantiated(); + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); + + callbackCaptor.getValue().run(); + + assertThat(handler1CallCount.get()).isEqualTo(1); + assertThat(handler2CallCount.get()).isEqualTo(1); + } + + @Test + public void givenConcurrentReloads_whenTriggered_thenShouldHandleThreadSafely() throws Exception { + SslBundles sslBundles = customizer.sslBundles(); + AtomicInteger handlerCallCount = new AtomicInteger(0); + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch doneLatch = new CountDownLatch(5); + + sslBundles.addBundleUpdateHandler("default", bundle -> handlerCallCount.incrementAndGet()); + + customizer.afterSingletonsInstantiated(); + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); + Runnable reloadCallback = callbackCaptor.getValue(); + + for (int i = 0; i < 5; i++) { + new Thread(() -> { + try { + startLatch.await(); + reloadCallback.run(); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + doneLatch.countDown(); + } + }).start(); + } + + startLatch.countDown(); + boolean completed = doneLatch.await(5, TimeUnit.SECONDS); + + assertThat(completed).isTrue(); + assertThat(handlerCallCount.get()).isEqualTo(5); + } + + @Test + public void givenReloadWithFailingCredentials_whenInvoked_thenShouldHandleGracefully() { + when(mockCredentialsConfig.getCredentials()).thenThrow(new RuntimeException("Failed to load credentials")); + + customizer.afterSingletonsInstantiated(); + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); + + callbackCaptor.getValue().run(); + } + + @Test + public void givenSslBundle_whenGetBundleMultipleTimes_thenShouldReturnFreshBundle() { + SslBundles sslBundles = customizer.sslBundles(); + + SslBundle bundle1 = sslBundles.getBundle("default"); + SslBundle bundle2 = sslBundles.getBundle("default"); + + assertThat(bundle1).isNotNull(); + assertThat(bundle2).isNotNull(); + } + + @Test + public void givenHttpServerSslCredentials_whenCreateBean_thenShouldReturnConfig() { + SslCredentialsConfig config = customizer.httpServerSslCredentials(); + + assertThat(config).isNotNull(); + assertThat(config.getName()).isEqualTo("HTTP Server SSL Credentials"); + assertThat(config.isTrustsOnly()).isFalse(); + } + +} diff --git a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java new file mode 100644 index 0000000000..0c9f1fc5b1 --- /dev/null +++ b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java @@ -0,0 +1,383 @@ +/** + * 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.common.transport.service; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.test.util.ReflectionTestUtils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CertificateReloadManagerTest { + + @TempDir + Path tempDir; + + private CertificateReloadManager certificateReloadManager; + private Path certFile; + + @BeforeEach + public void setup() throws IOException { + certificateReloadManager = new CertificateReloadManager(); + + certFile = tempDir.resolve("test-cert.pem"); + Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nTEST_CERT_V1\n-----END CERTIFICATE-----\n"); + } + + @AfterEach + public void teardown() throws Exception { + if (certificateReloadManager != null) { + certificateReloadManager.destroy(); + } + } + + @Test + public void givenCertificateFileChanged_whenCheckForChanges_thenShouldTriggerReload() throws Exception { + CountDownLatch reloadLatch = new CountDownLatch(1); + AtomicInteger reloadCount = new AtomicInteger(0); + + certificateReloadManager.registerWatcher("test-cert", certFile, () -> { + reloadCount.incrementAndGet(); + reloadLatch.countDown(); + }); + + Thread.sleep(100); + Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nTEST_CERT_V2_MODIFIED\n-----END CERTIFICATE-----\n"); + + ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); + + boolean reloadTriggered = reloadLatch.await(2, TimeUnit.SECONDS); + + assertThat(reloadTriggered).isTrue(); + assertThat(reloadCount.get()).isEqualTo(1); + } + + @Test + public void givenCertificateFileUnchanged_whenCheckForChanges_thenShouldNotTriggerReload() throws Exception { + AtomicInteger reloadCount = new AtomicInteger(0); + + certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); + + Thread.sleep(100); + ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); + Thread.sleep(100); + ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); + + assertThat(reloadCount.get()).isEqualTo(0); + } + + @Test + public void givenOnlyTimestampChanged_whenCheckForChanges_thenShouldNotTriggerReload() throws Exception { + AtomicInteger reloadCount = new AtomicInteger(0); + + certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); + + Thread.sleep(100); + + ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); + + assertThat(reloadCount.get()).isEqualTo(0); + } + + @Test + public void givenWatcherRegistered_whenFileDeleted_thenShouldNotCrash() throws Exception { + AtomicInteger reloadCount = new AtomicInteger(0); + + certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); + + Thread.sleep(100); + + Files.delete(certFile); + Thread.sleep(100); + + ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); + Thread.sleep(100); + + // File deletion changes checksum from real hash to "", so reload is triggered + assertThat(reloadCount.get()).isEqualTo(1); + } + + @Test + public void givenWatcherRegistered_whenShutdown_thenShouldStopScheduler() throws Exception { + certificateReloadManager.registerWatcher("test-cert", certFile, () -> {}); + + certificateReloadManager.destroy(); + + assertThat(certificateReloadManager).isNotNull(); + } + + @Test + public void givenMultipleCertificateFiles_whenOneChanges_thenShouldTriggerReload() throws Exception { + Path keyFile = tempDir.resolve("test-key.pem"); + Files.writeString(keyFile, "-----BEGIN PRIVATE KEY-----\nTEST_KEY_V1\n-----END PRIVATE KEY-----\n"); + + CountDownLatch certReloadLatch = new CountDownLatch(1); + CountDownLatch keyReloadLatch = new CountDownLatch(1); + + certificateReloadManager.registerWatcher("test-cert", certFile, certReloadLatch::countDown); + certificateReloadManager.registerWatcher("test-key", keyFile, keyReloadLatch::countDown); + + Thread.sleep(100); + Files.writeString(keyFile, "-----BEGIN PRIVATE KEY-----\nTEST_KEY_V2_MODIFIED\n-----END PRIVATE KEY-----\n"); + Thread.sleep(100); + + ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); + + boolean keyReloaded = keyReloadLatch.await(2, TimeUnit.SECONDS); + + assertThat(keyReloaded).isTrue(); + assertThat(certReloadLatch.getCount()).isEqualTo(1); + } + + @Test + public void givenMultipleWatchers_whenCheckCertificates_thenShouldCheckAll() throws Exception { + Path cert2File = tempDir.resolve("test-cert2.pem"); + Files.writeString(cert2File, "-----BEGIN CERTIFICATE-----\nTEST_CERT2_V1\n-----END CERTIFICATE-----\n"); + + AtomicInteger reload1Count = new AtomicInteger(0); + AtomicInteger reload2Count = new AtomicInteger(0); + + certificateReloadManager.registerWatcher("test-cert1", certFile, reload1Count::incrementAndGet); + certificateReloadManager.registerWatcher("test-cert2", cert2File, reload2Count::incrementAndGet); + + Thread.sleep(100); + Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED1\n-----END CERTIFICATE-----\n"); + Files.writeString(cert2File, "-----BEGIN CERTIFICATE-----\nMODIFIED2\n-----END CERTIFICATE-----\n"); + Thread.sleep(100); + + ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); + + Thread.sleep(200); + assertThat(reload1Count.get()).isEqualTo(1); + assertThat(reload2Count.get()).isEqualTo(1); + } + + @Test + public void givenCallbackThrowsException_whenCheckForChanges_thenShouldContinueWithOtherWatchers() throws Exception { + Path cert2File = tempDir.resolve("test-cert2.pem"); + Files.writeString(cert2File, "-----BEGIN CERTIFICATE-----\nTEST_CERT2_V1\n-----END CERTIFICATE-----\n"); + + AtomicInteger reload2Count = new AtomicInteger(0); + + certificateReloadManager.registerWatcher("test-cert1", certFile, () -> { + throw new RuntimeException("Simulated reload failure"); + }); + certificateReloadManager.registerWatcher("test-cert2", cert2File, reload2Count::incrementAndGet); + + Thread.sleep(100); + Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED1\n-----END CERTIFICATE-----\n"); + Files.writeString(cert2File, "-----BEGIN CERTIFICATE-----\nMODIFIED2\n-----END CERTIFICATE-----\n"); + Thread.sleep(100); + + ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); + + Thread.sleep(200); + assertThat(reload2Count.get()).isEqualTo(1); + } + + @Test + public void givenFileDeletedAndRecreated_whenCheckForChanges_thenShouldTriggerReload() throws Exception { + AtomicInteger reloadCount = new AtomicInteger(0); + + certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); + + Thread.sleep(100); + + Files.delete(certFile); + Thread.sleep(100); + + ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); + + Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nNEW_CERT\n-----END CERTIFICATE-----\n"); + Thread.sleep(100); + + ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); + + Thread.sleep(200); + assertThat(reloadCount.get()).isEqualTo(2); + } + + @Test + public void givenRapidFileModifications_whenCheckForChanges_thenShouldDetectLatestChange() throws Exception { + CountDownLatch reloadLatch = new CountDownLatch(1); + AtomicInteger reloadCount = new AtomicInteger(0); + + certificateReloadManager.registerWatcher("test-cert", certFile, () -> { + reloadCount.incrementAndGet(); + reloadLatch.countDown(); + }); + + Thread.sleep(100); + + for (int i = 0; i < 5; i++) { + Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nCERT_VERSION_" + i + "\n-----END CERTIFICATE-----\n"); + } + Thread.sleep(100); + + ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); + + boolean reloadTriggered = reloadLatch.await(2, TimeUnit.SECONDS); + + assertThat(reloadTriggered).isTrue(); + assertThat(reloadCount.get()).isEqualTo(1); + } + + @Test + public void givenConcurrentChecks_whenCheckForChanges_thenShouldReloadExactlyOnce() throws Exception { + AtomicInteger reloadCount = new AtomicInteger(0); + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch doneLatch = new CountDownLatch(5); + + certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); + + Thread.sleep(100); + Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED\n-----END CERTIFICATE-----\n"); + Thread.sleep(100); + + for (int i = 0; i < 5; i++) { + new Thread(() -> { + try { + startLatch.await(); + ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + doneLatch.countDown(); + } + }).start(); + } + + startLatch.countDown(); + boolean completed = doneLatch.await(5, TimeUnit.SECONDS); + + assertThat(completed).isTrue(); + // With atomic checkAndReload, exactly one reload should happen + assertThat(reloadCount.get()).isEqualTo(1); + } + + @Test + public void givenSameContentRewritten_whenCheckForChanges_thenShouldNotTriggerReload() throws Exception { + AtomicInteger reloadCount = new AtomicInteger(0); + String originalContent = Files.readString(certFile); + + certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); + + Thread.sleep(100); + + Files.writeString(certFile, originalContent); + Thread.sleep(100); + + ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); + + Thread.sleep(200); + assertThat(reloadCount.get()).isEqualTo(0); + } + + @Test + public void givenCallbackFailsRepeatedly_whenMaxFailuresReached_thenShouldStopRetrying() throws Exception { + AtomicInteger reloadAttempts = new AtomicInteger(0); + + certificateReloadManager.registerWatcher("test-cert", certFile, () -> { + reloadAttempts.incrementAndGet(); + throw new RuntimeException("Persistent failure"); + }); + + Thread.sleep(100); + Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n"); + Thread.sleep(100); + + // Retry up to MAX_CONSECUTIVE_FAILURES (10) + a few extra to confirm it stops + for (int i = 0; i < 15; i++) { + ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); + } + + assertThat(reloadAttempts.get()).isEqualTo(10); + } + + @Test + public void givenCallbackFailedPreviously_whenFileChangesAgain_thenShouldResetAndRetry() throws Exception { + AtomicInteger reloadAttempts = new AtomicInteger(0); + AtomicInteger shouldFail = new AtomicInteger(1); + + certificateReloadManager.registerWatcher("test-cert", certFile, () -> { + reloadAttempts.incrementAndGet(); + if (shouldFail.get() == 1) { + throw new RuntimeException("Transient failure"); + } + }); + + Thread.sleep(100); + Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n"); + Thread.sleep(100); + + // First attempt fails + ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); + assertThat(reloadAttempts.get()).isEqualTo(1); + + // Fix the callback and change the file to new content + shouldFail.set(0); + Thread.sleep(100); + Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nGOOD_CERT\n-----END CERTIFICATE-----\n"); + Thread.sleep(100); + + // Should reset failure counter and succeed because file content changed + ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); + assertThat(reloadAttempts.get()).isEqualTo(2); + } + + @Test + public void givenCallbackHitMaxFailures_whenFileChangesToNewContent_thenShouldResetAndRetry() throws Exception { + AtomicInteger reloadAttempts = new AtomicInteger(0); + AtomicInteger shouldFail = new AtomicInteger(1); + + certificateReloadManager.registerWatcher("test-cert", certFile, () -> { + reloadAttempts.incrementAndGet(); + if (shouldFail.get() == 1) { + throw new RuntimeException("Persistent failure"); + } + }); + + Thread.sleep(100); + Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n"); + Thread.sleep(100); + + // Exhaust all retries + for (int i = 0; i < 15; i++) { + ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); + } + assertThat(reloadAttempts.get()).isEqualTo(10); + + // Fix callback and change file to new content + shouldFail.set(0); + Thread.sleep(100); + Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nFIXED_CERT\n-----END CERTIFICATE-----\n"); + Thread.sleep(100); + + // Should detect new content, reset counter, and succeed + ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); + assertThat(reloadAttempts.get()).isEqualTo(11); + } + +} diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index 9ab06ca996..c7283b9c0c 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -170,6 +170,15 @@ transport: enabled: "${TB_TRANSPORT_STATS_ENABLED:true}" # Interval of transport statistics logging print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" + ssl: + # SSL/TLS settings for the transport layer + certificate: + # X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.) + reload: + # Enable/disable automatic SSL certificates reload + enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" + # Check interval in seconds for certificates reload + check_interval: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL:60}" # CoAP server parameters coap: diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index f869534088..d9ca77d8af 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -201,6 +201,15 @@ transport: enabled: "${TB_TRANSPORT_STATS_ENABLED:true}" # Interval of transport statistics logging print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" + ssl: + # SSL/TLS settings for the transport layer + certificate: + # X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.) + reload: + # Enable/disable automatic SSL certificates reload + enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" + # Check interval in seconds for certificates reload + check_interval: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL:60}" # Queue configuration parameters queue: diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index d22bd34505..12dbf24298 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -301,6 +301,15 @@ transport: enabled: "${TB_TRANSPORT_STATS_ENABLED:true}" # Interval of transport statistics logging print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" + ssl: + # SSL/TLS settings for the transport layer + certificate: + # X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.) + reload: + # Enable/disable automatic SSL certificates reload + enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" + # Check interval in seconds for certificates reload + check_interval: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL:60}" # Queue configuration properties queue: diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index ccbd3901ce..7be3879efe 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -234,6 +234,15 @@ transport: max_wrong_credentials_per_ip: "${TB_TRANSPORT_MAX_WRONG_CREDENTIALS_PER_IP:10}" # Timeout to expire block IP addresses ip_block_timeout: "${TB_TRANSPORT_IP_BLOCK_TIMEOUT:60000}" + ssl: + # SSL/TLS settings for the transport layer + certificate: + # X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.) + reload: + # Enable/disable automatic SSL certificates reload + enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" + # Check interval in seconds for certificates reload + check_interval: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL:60}" # Queue configuration parameters queue: From 1d6594161b1a4f6069b5204b87376eb3663414a8 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 25 Mar 2026 15:28:28 +0200 Subject: [PATCH 02/25] fix: force getLhServer() failure in LwM2m listener preservation test --- .../lwm2m/server/LwM2mServerCertificateReloadTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerCertificateReloadTest.java b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerCertificateReloadTest.java index 15e4d07622..c8b7f0d060 100644 --- a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerCertificateReloadTest.java +++ b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerCertificateReloadTest.java @@ -159,6 +159,9 @@ public class LwM2mServerCertificateReloadTest { LwM2mServerListener serverListener = new LwM2mServerListener(mockHandler); ReflectionTestUtils.setField(lwm2mTransportService, "serverListener", serverListener); + // Force getLhServer() to fail by returning null host + when(mockConfig.getHost()).thenReturn(null); + // Invoke the callback — new server creation will fail, old listeners should stay callbackCaptor.getValue().run(); From 255bb38dc2c0a3265376ce1a49dd1c5a0d905804 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 25 Mar 2026 15:44:10 +0200 Subject: [PATCH 03/25] Fimprove SSL certificate reload robustness and config clarity --- application/src/main/resources/thingsboard.yml | 2 +- .../lwm2m/server/DefaultLwM2mTransportService.java | 8 +++++++- .../ssl/SslCredentialsWebServerCustomizer.java | 2 +- .../service/CertificateReloadManager.java | 14 ++++++++++---- .../coap/src/main/resources/tb-coap-transport.yml | 2 +- .../http/src/main/resources/tb-http-transport.yml | 2 +- .../src/main/resources/tb-lwm2m-transport.yml | 2 +- .../mqtt/src/main/resources/tb-mqtt-transport.yml | 2 +- 8 files changed, 23 insertions(+), 11 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 419e6d3dfd..3001ea4187 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1402,7 +1402,7 @@ transport: # Enable/disable automatic SSL certificates reload enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" # Check interval in seconds for certificates reload - check_interval: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL:60}" + check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}" # CoAP server parameters coap: diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java index 21fdc07277..21b90f8d0c 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java @@ -235,7 +235,13 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService, Smar log.info("Creating new LwM2M server with updated certificates..."); LeshanServer newServer = getLhServer(); - newServer.start(); + try { + newServer.start(); + } catch (Exception e) { + log.error("Failed to start new LwM2M server, rolling back", e); + newServer.destroy(); + throw e; + } try { LwM2mServerListener newListener = new LwM2mServerListener(handler); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsWebServerCustomizer.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsWebServerCustomizer.java index 147daefa7e..d213d9dc86 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsWebServerCustomizer.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsWebServerCustomizer.java @@ -147,7 +147,7 @@ public class SslCredentialsWebServerCustomizer implements WebServerFactoryCustom @Override public void addBundleRegisterHandler(BiConsumer registerHandler) { - + log.debug("addBundleRegisterHandler is not supported for dynamic SSL bundles"); } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java index 19fc545d0b..9940046b7d 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig; import org.thingsboard.server.queue.util.TbTransportComponent; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.security.MessageDigest; @@ -50,7 +51,7 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis @Value("${transport.ssl.certificate.reload.enabled:true}") private boolean reloadEnabled; - @Value("${transport.ssl.certificate.reload.check_interval:60}") + @Value("${transport.ssl.certificate.reload.check_interval_seconds:60}") private long checkIntervalInSeconds; @Autowired @@ -258,9 +259,14 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis return ""; } MessageDigest md = MessageDigest.getInstance("SHA-256"); - byte[] bytes = Files.readAllBytes(path); - byte[] hash = md.digest(bytes); - return Base64.getEncoder().encodeToString(hash); + byte[] buf = new byte[8192]; + try (InputStream is = Files.newInputStream(path)) { + int bytesRead; + while ((bytesRead = is.read(buf)) != -1) { + md.update(buf, 0, bytesRead); + } + } + return Base64.getEncoder().encodeToString(md.digest()); } catch (Exception e) { log.warn("Failed to calculate checksum for certificate file: {}", path, e); return ""; diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index c7283b9c0c..3c1ef94f09 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -178,7 +178,7 @@ transport: # Enable/disable automatic SSL certificates reload enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" # Check interval in seconds for certificates reload - check_interval: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL:60}" + check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}" # CoAP server parameters coap: diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index d9ca77d8af..1b221d1fd9 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -209,7 +209,7 @@ transport: # Enable/disable automatic SSL certificates reload enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" # Check interval in seconds for certificates reload - check_interval: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL:60}" + check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}" # Queue configuration parameters queue: diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 12dbf24298..6140122062 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -309,7 +309,7 @@ transport: # Enable/disable automatic SSL certificates reload enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" # Check interval in seconds for certificates reload - check_interval: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL:60}" + check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}" # Queue configuration properties queue: diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index 7be3879efe..ac02fa396b 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -242,7 +242,7 @@ transport: # Enable/disable automatic SSL certificates reload enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" # Check interval in seconds for certificates reload - check_interval: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL:60}" + check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}" # Queue configuration parameters queue: From c2a8f79edd42c421a86567c46443a7872c6bb362 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 25 Mar 2026 15:54:02 +0200 Subject: [PATCH 04/25] Minor changing for CertificateReloadManager --- .../server/coapserver/DefaultCoapServerService.java | 2 +- .../server/common/data/ResourceUtils.java | 5 ++++- .../server/common/data/ResourceUtilsTest.java | 9 +++++---- .../bootstrap/LwM2MTransportBootstrapService.java | 2 +- .../lwm2m/server/DefaultLwM2mTransportService.java | 4 ++-- .../ssl/SslCredentialsWebServerCustomizer.java | 3 ++- .../transport/service/CertificateReloadManager.java | 12 +++++++++--- 7 files changed, 24 insertions(+), 13 deletions(-) diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java index 7882125906..b073e34202 100644 --- a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java @@ -214,12 +214,12 @@ public class DefaultCoapServerService implements CoapServerService, SmartInitial if (oldDtlsEndpoint != null) { log.info("Stopping old DTLS endpoint..."); + server.getEndpoints().remove(oldDtlsEndpoint); oldDtlsEndpoint.stop(); if (oldDtlsConnector != null) { oldDtlsConnector.destroy(); } oldDtlsEndpoint.destroy(); - server.getEndpoints().remove(oldDtlsEndpoint); log.info("Old DTLS endpoint stopped and destroyed."); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceUtils.java index 626d0cd372..13c75feed0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceUtils.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceUtils.java @@ -111,7 +111,10 @@ public class ResourceUtils { return resourceFile.getAbsolutePath(); } else { URL url = classLoader.getResource(filePath); - return url != null ? url.toURI().toString() : null; + if (url == null) { + throw new RuntimeException("Unable to find resource: " + filePath); + } + return url.toURI().toString(); } } catch (Exception e) { if (e instanceof NullPointerException) { diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/ResourceUtilsTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/ResourceUtilsTest.java index 8c91762068..8fb6214dac 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/ResourceUtilsTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/ResourceUtilsTest.java @@ -18,14 +18,15 @@ package org.thingsboard.server.common.data; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; class ResourceUtilsTest { @Test - public void givenNonExistentResource_whenGetUri_thenReturnsNull() { - String result = ResourceUtils.getUri(ResourceUtilsTest.class.getClassLoader(), "non/existent/resource/path.txt"); - - assertThat(result).isNull(); + public void givenNonExistentResource_whenGetUri_thenThrowsRuntimeException() { + assertThatThrownBy(() -> ResourceUtils.getUri(ResourceUtilsTest.class.getClassLoader(), "non/existent/resource/path.txt")) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("Unable to find resource"); } @Test diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java index 78f292d69c..6de8acde90 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java @@ -64,7 +64,7 @@ public class LwM2MTransportBootstrapService implements SmartInitializingSingleto private final LwM2MInMemoryBootstrapConfigStore lwM2MInMemoryBootstrapConfigStore; private final TransportService transportService; private final TbLwM2MDtlsBootstrapCertificateVerifier certificateVerifier; - private LeshanBootstrapServer server; + private volatile LeshanBootstrapServer server; @Override public void afterSingletonsInstantiated() { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java index 21b90f8d0c..91b3fb072f 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java @@ -84,8 +84,8 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService, Smar private final TbLwM2MAuthorizer authorizer; private final LwM2mVersionedModelProvider modelProvider; - private LeshanServer server; - private LwM2mServerListener serverListener; + private volatile LeshanServer server; + private volatile LwM2mServerListener serverListener; @Override public void afterSingletonsInstantiated() { diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsWebServerCustomizer.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsWebServerCustomizer.java index d213d9dc86..2a291b3888 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsWebServerCustomizer.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsWebServerCustomizer.java @@ -22,6 +22,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.ssl.NoSuchSslBundleException; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.ssl.SslStoreBundle; @@ -125,7 +126,7 @@ public class SslCredentialsWebServerCustomizer implements WebServerFactoryCustom @Override public SslBundle getBundle(String name) { if (!DEFAULT_BUNDLE_NAME.equals(name)) { - throw new IllegalArgumentException("Unknown SSL bundle: " + name); + throw new NoSuchSslBundleException(name, "Unknown SSL bundle: " + name); } return createSslBundle(); } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java index 9940046b7d..a514b110cb 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java @@ -196,14 +196,20 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis for (Path path : paths) { String checksum = calculateChecksum(path); currentChecksums.put(path, checksum); - combined.append(checksum); + if (!combined.isEmpty()) { + combined.append("|"); + } + combined.append(path).append("=").append(checksum); } String combinedChecksum = combined.toString(); // Build old combined checksum for comparison StringBuilder oldCombined = new StringBuilder(); for (Path path : paths) { - oldCombined.append(lastChecksumMap.getOrDefault(path, "")); + if (!oldCombined.isEmpty()) { + oldCombined.append("|"); + } + oldCombined.append(path).append("=").append(lastChecksumMap.getOrDefault(path, "")); } String oldCombinedChecksum = oldCombined.toString(); @@ -216,7 +222,7 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis } if (!combinedChecksum.equals(failedCombinedChecksum) && consecutiveFailures > 0) { - // File content changed since last failure — reset and retry + // File content has changed since the last failure - reset and retry consecutiveFailures = 0; failedCombinedChecksum = null; } From edcf3a9d2295d9a8d425fc8750de4a7bd3f8f3e6 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 25 Mar 2026 16:01:34 +0200 Subject: [PATCH 05/25] Fix SSL certificate reload: port conflict, redundant reload(), and watcher polling --- .../coapserver/DefaultCoapServerService.java | 25 +++++++++++++++---- .../CoapDtlsCertificateReloadTest.java | 9 ++++--- .../config/ssl/AbstractSslCredentials.java | 3 +-- .../service/CertificateReloadManager.java | 4 +++ 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java index b073e34202..34e7e2cfde 100644 --- a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java @@ -195,14 +195,31 @@ public class DefaultCoapServerService implements CoapServerService, SmartInitial DTLSConnector newConnector = createDtlsConnector(dtlsConnectorConfig); CoapEndpoint newEndpoint = buildDtlsEndpoint(networkConfig, newConnector); + // Stop old endpoint first to release the port before starting the new one + if (oldDtlsEndpoint != null) { + log.info("Stopping old DTLS endpoint to release the port..."); + server.getEndpoints().remove(oldDtlsEndpoint); + oldDtlsEndpoint.stop(); + } + server.addEndpoint(newEndpoint); try { newEndpoint.start(); } catch (IOException e) { - log.error("Failed to start new DTLS endpoint, cleaning up", e); + log.error("Failed to start new DTLS endpoint, restoring old endpoint", e); server.getEndpoints().remove(newEndpoint); newEndpoint.destroy(); newConnector.destroy(); + // Attempt to restore the old endpoint + if (oldDtlsEndpoint != null) { + try { + server.addEndpoint(oldDtlsEndpoint); + oldDtlsEndpoint.start(); + log.info("Old DTLS endpoint restored successfully."); + } catch (IOException restoreEx) { + log.error("Failed to restore old DTLS endpoint", restoreEx); + } + } throw e; } log.info("New DTLS endpoint started successfully."); @@ -212,15 +229,13 @@ public class DefaultCoapServerService implements CoapServerService, SmartInitial dtlsCoapEndpoint = newEndpoint; tbDtlsCertificateVerifier = (TbCoapDtlsCertificateVerifier) dtlsConnectorConfig.getAdvancedCertificateVerifier(); + // Destroy old resources after successful swap if (oldDtlsEndpoint != null) { - log.info("Stopping old DTLS endpoint..."); - server.getEndpoints().remove(oldDtlsEndpoint); - oldDtlsEndpoint.stop(); if (oldDtlsConnector != null) { oldDtlsConnector.destroy(); } oldDtlsEndpoint.destroy(); - log.info("Old DTLS endpoint stopped and destroyed."); + log.info("Old DTLS endpoint destroyed."); } } diff --git a/common/coap-server/src/test/java/org/thingsboard/server/coapserver/CoapDtlsCertificateReloadTest.java b/common/coap-server/src/test/java/org/thingsboard/server/coapserver/CoapDtlsCertificateReloadTest.java index 642f2e0be9..a7413bdadd 100644 --- a/common/coap-server/src/test/java/org/thingsboard/server/coapserver/CoapDtlsCertificateReloadTest.java +++ b/common/coap-server/src/test/java/org/thingsboard/server/coapserver/CoapDtlsCertificateReloadTest.java @@ -189,7 +189,7 @@ public class CoapDtlsCertificateReloadTest { } @Test - public void givenReloadCallback_whenStartFails_thenNewResourcesCleaned() throws Exception { + public void givenReloadCallback_whenStartFails_thenNewResourcesCleanedAndOldRestored() throws Exception { // GIVEN when(mockCoapServerContext.getDtlsSettings()).thenReturn(mockDtlsSettings); @@ -210,6 +210,7 @@ public class CoapDtlsCertificateReloadTest { doReturn(mockNewEndpoint).when(spyService).buildDtlsEndpoint(any(Configuration.class), any(DTLSConnector.class)); List endpointsList = new CopyOnWriteArrayList<>(); + endpointsList.add(mockDtlsEndpoint); when(mockCoapServer.getEndpoints()).thenReturn(endpointsList); // WHEN - the callback catches the IOException internally @@ -224,12 +225,12 @@ public class CoapDtlsCertificateReloadTest { verify(mockNewEndpoint).destroy(); verify(mockNewConnector).destroy(); assertThat(endpointsList).doesNotContain(mockNewEndpoint); + // Old endpoint was stopped to release port, then restored after new one failed + verify(mockDtlsEndpoint).stop(); + verify(mockDtlsEndpoint).start(); // Old fields preserved assertThat(ReflectionTestUtils.getField(spyService, "dtlsCoapEndpoint")).isSameAs(mockDtlsEndpoint); assertThat(ReflectionTestUtils.getField(spyService, "dtlsConnector")).isSameAs(mockDtlsConnector); - // Old endpoint not touched - verify(mockDtlsEndpoint, never()).stop(); - verify(mockDtlsEndpoint, never()).destroy(); } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/AbstractSslCredentials.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/AbstractSslCredentials.java index 9ffa4f7aac..55e6f63e5d 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/AbstractSslCredentials.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/AbstractSslCredentials.java @@ -60,8 +60,7 @@ public abstract class AbstractSslCredentials implements SslCredentials { @Override public void reload(boolean trustsOnly) throws IOException, GeneralSecurityException { - SslState newState = buildState(trustsOnly); - state.set(newState); + init(trustsOnly); } private SslState buildState(boolean trustsOnly) throws IOException, GeneralSecurityException { diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java index a514b110cb..0dd1fae724 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java @@ -228,6 +228,10 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis } if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) { + // Update modification times to avoid re-checking mtime and re-computing checksums every poll cycle + for (Path path : paths) { + lastModifiedMap.put(path, getLastModifiedTime(path)); + } return; } From b431e4c0e605d288d954e8237973506b8fc48a13 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 25 Mar 2026 16:11:50 +0200 Subject: [PATCH 06/25] Fix LwM2M server recreation: stop old server before starting new to avoid port conflict --- .../LwM2MTransportBootstrapService.java | 26 ++++++-- .../server/DefaultLwM2mTransportService.java | 63 ++++++++++-------- .../LwM2mBootstrapCertificateReloadTest.java | 19 ++++-- .../service/CertificateReloadManagerTest.java | 64 +++++++++---------- 4 files changed, 101 insertions(+), 71 deletions(-) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java index 6de8acde90..a9a4e364b5 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java @@ -184,21 +184,33 @@ public class LwM2MTransportBootstrapService implements SmartInitializingSingleto log.info("Creating new LwM2M Bootstrap server with updated certificates..."); LeshanBootstrapServer newServer = getLhBootstrapServer(); + + // Stop the old server first to release the ports before starting the new one + if (oldServer != null) { + log.info("Stopping old LwM2M Bootstrap server to release ports..."); + oldServer.destroy(); + } + try { newServer.start(); } catch (Exception e) { - log.error("Failed to start new LwM2M Bootstrap server, rolling back", e); + log.error("Failed to start new LwM2M Bootstrap server", e); newServer.destroy(); + // Attempt to restore the old server + if (oldServer != null) { + try { + LeshanBootstrapServer restoredServer = getLhBootstrapServer(); + restoredServer.start(); + this.server = restoredServer; + log.info("Restored LwM2M Bootstrap server with previous configuration."); + } catch (Exception restoreEx) { + log.error("Failed to restore old LwM2M Bootstrap server", restoreEx); + } + } throw e; } this.server = newServer; log.info("New LwM2M Bootstrap server started successfully."); - - if (oldServer != null) { - log.info("Stopping old LwM2M Bootstrap server..."); - oldServer.destroy(); - log.info("Old LwM2M Bootstrap server stopped."); - } } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java index 91b3fb072f..b0d25729b3 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java @@ -235,33 +235,10 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService, Smar log.info("Creating new LwM2M server with updated certificates..."); LeshanServer newServer = getLhServer(); - try { - newServer.start(); - } catch (Exception e) { - log.error("Failed to start new LwM2M server, rolling back", e); - newServer.destroy(); - throw e; - } - - try { - LwM2mServerListener newListener = new LwM2mServerListener(handler); - newServer.getRegistrationService().addListener(newListener.registrationListener); - newServer.getPresenceService().addListener(newListener.presenceListener); - newServer.getObservationService().addListener(newListener.observationListener); - newServer.getSendService().addListener(newListener.sendListener); - - this.server = newServer; - this.context.setServer(newServer); - this.serverListener = newListener; - } catch (Exception e) { - log.error("Failed to register listeners on new LwM2M server, rolling back", e); - newServer.destroy(); - throw e; - } - log.info("New LwM2M server started successfully."); + // Stop old server first to release the ports before starting the new one if (oldServer != null) { - log.info("Stopping old LwM2M server..."); + log.info("Stopping old LwM2M server to release ports..."); if (oldListener != null) { oldServer.getRegistrationService().removeListener(oldListener.registrationListener); oldServer.getPresenceService().removeListener(oldListener.presenceListener); @@ -269,8 +246,42 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService, Smar oldServer.getSendService().removeListener(oldListener.sendListener); } oldServer.destroy(); - log.info("Old LwM2M server stopped."); } + + try { + newServer.start(); + } catch (Exception e) { + log.error("Failed to start new LwM2M server", e); + newServer.destroy(); + // Attempt to restore the old server + try { + LeshanServer restoredServer = getLhServer(); + restoredServer.start(); + LwM2mServerListener restoredListener = new LwM2mServerListener(handler); + restoredServer.getRegistrationService().addListener(restoredListener.registrationListener); + restoredServer.getPresenceService().addListener(restoredListener.presenceListener); + restoredServer.getObservationService().addListener(restoredListener.observationListener); + restoredServer.getSendService().addListener(restoredListener.sendListener); + this.server = restoredServer; + this.context.setServer(restoredServer); + this.serverListener = restoredListener; + log.info("Restored LwM2M server with previous configuration."); + } catch (Exception restoreEx) { + log.error("Failed to restore old LwM2M server", restoreEx); + } + throw e; + } + + LwM2mServerListener newListener = new LwM2mServerListener(handler); + newServer.getRegistrationService().addListener(newListener.registrationListener); + newServer.getPresenceService().addListener(newListener.presenceListener); + newServer.getObservationService().addListener(newListener.observationListener); + newServer.getSendService().addListener(newListener.sendListener); + + this.server = newServer; + this.context.setServer(newServer); + this.serverListener = newListener; + log.info("New LwM2M server started successfully."); } @Override diff --git a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2mBootstrapCertificateReloadTest.java b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2mBootstrapCertificateReloadTest.java index 51d37ca739..84bc44b03b 100644 --- a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2mBootstrapCertificateReloadTest.java +++ b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2mBootstrapCertificateReloadTest.java @@ -118,8 +118,8 @@ public class LwM2mBootstrapCertificateReloadTest { Runnable reloadCallback = callbackCaptor.getValue(); - // getLhBootstrapServer() will fail due to null host. - // With create-then-swap, the old server should NOT be destroyed. + // getLhBootstrapServer() will fail due to null host before old server is stopped. + // The old server should NOT be destroyed since the new server was never created. reloadCallback.run(); verify(mockBootstrapServer, never()).destroy(); @@ -164,15 +164,18 @@ public class LwM2mBootstrapCertificateReloadTest { } @Test - public void givenReloadCallback_whenNewServerStartFails_thenNewServerDestroyedAndOldPreserved() { + public void givenReloadCallback_whenNewServerStartFails_thenNewServerDestroyedAndRestorationAttempted() { // GIVEN ReflectionTestUtils.setField(bootstrapService, "server", mockBootstrapServer); LeshanBootstrapServer mockNewServer = mock(LeshanBootstrapServer.class); doThrow(new RuntimeException("start failed")).when(mockNewServer).start(); + LeshanBootstrapServer mockRestoredServer = mock(LeshanBootstrapServer.class); + LwM2MTransportBootstrapService spyService = Mockito.spy(bootstrapService); - doReturn(mockNewServer).when(spyService).getLhBootstrapServer(); + // First call returns the failing server, second call returns the restoration server + doReturn(mockNewServer).doReturn(mockRestoredServer).when(spyService).getLhBootstrapServer(); ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); spyService.afterSingletonsInstantiated(); @@ -184,9 +187,13 @@ public class LwM2mBootstrapCertificateReloadTest { reloadCallback.run(); // THEN + // Old server is destroyed to release ports + verify(mockBootstrapServer).destroy(); + // New server fails to start and is destroyed verify(mockNewServer).destroy(); - assertThat(ReflectionTestUtils.getField(spyService, "server")).isSameAs(mockBootstrapServer); - verify(mockBootstrapServer, never()).destroy(); + // Restoration server is started and becomes the active server + verify(mockRestoredServer).start(); + assertThat(ReflectionTestUtils.getField(spyService, "server")).isSameAs(mockRestoredServer); } } diff --git a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java index 0c9f1fc5b1..ba3aa66c70 100644 --- a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java +++ b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java @@ -63,7 +63,7 @@ public class CertificateReloadManagerTest { reloadLatch.countDown(); }); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nTEST_CERT_V2_MODIFIED\n-----END CERTIFICATE-----\n"); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -80,9 +80,9 @@ public class CertificateReloadManagerTest { certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); assertThat(reloadCount.get()).isEqualTo(0); @@ -94,7 +94,7 @@ public class CertificateReloadManagerTest { certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -107,13 +107,13 @@ public class CertificateReloadManagerTest { certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); Files.delete(certFile); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); // File deletion changes checksum from real hash to "", so reload is triggered assertThat(reloadCount.get()).isEqualTo(1); @@ -139,9 +139,9 @@ public class CertificateReloadManagerTest { certificateReloadManager.registerWatcher("test-cert", certFile, certReloadLatch::countDown); certificateReloadManager.registerWatcher("test-key", keyFile, keyReloadLatch::countDown); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); Files.writeString(keyFile, "-----BEGIN PRIVATE KEY-----\nTEST_KEY_V2_MODIFIED\n-----END PRIVATE KEY-----\n"); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -162,10 +162,10 @@ public class CertificateReloadManagerTest { certificateReloadManager.registerWatcher("test-cert1", certFile, reload1Count::incrementAndGet); certificateReloadManager.registerWatcher("test-cert2", cert2File, reload2Count::incrementAndGet); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED1\n-----END CERTIFICATE-----\n"); Files.writeString(cert2File, "-----BEGIN CERTIFICATE-----\nMODIFIED2\n-----END CERTIFICATE-----\n"); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -186,10 +186,10 @@ public class CertificateReloadManagerTest { }); certificateReloadManager.registerWatcher("test-cert2", cert2File, reload2Count::incrementAndGet); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED1\n-----END CERTIFICATE-----\n"); Files.writeString(cert2File, "-----BEGIN CERTIFICATE-----\nMODIFIED2\n-----END CERTIFICATE-----\n"); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -203,15 +203,15 @@ public class CertificateReloadManagerTest { certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); Files.delete(certFile); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nNEW_CERT\n-----END CERTIFICATE-----\n"); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -229,12 +229,12 @@ public class CertificateReloadManagerTest { reloadLatch.countDown(); }); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); for (int i = 0; i < 5; i++) { Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nCERT_VERSION_" + i + "\n-----END CERTIFICATE-----\n"); } - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -252,9 +252,9 @@ public class CertificateReloadManagerTest { certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED\n-----END CERTIFICATE-----\n"); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); for (int i = 0; i < 5; i++) { new Thread(() -> { @@ -284,10 +284,10 @@ public class CertificateReloadManagerTest { certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); Files.writeString(certFile, originalContent); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -304,9 +304,9 @@ public class CertificateReloadManagerTest { throw new RuntimeException("Persistent failure"); }); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n"); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); // Retry up to MAX_CONSECUTIVE_FAILURES (10) + a few extra to confirm it stops for (int i = 0; i < 15; i++) { @@ -328,9 +328,9 @@ public class CertificateReloadManagerTest { } }); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n"); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); // First attempt fails ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -338,9 +338,9 @@ public class CertificateReloadManagerTest { // Fix the callback and change the file to new content shouldFail.set(0); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nGOOD_CERT\n-----END CERTIFICATE-----\n"); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); // Should reset failure counter and succeed because file content changed ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -359,9 +359,9 @@ public class CertificateReloadManagerTest { } }); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n"); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); // Exhaust all retries for (int i = 0; i < 15; i++) { @@ -371,9 +371,9 @@ public class CertificateReloadManagerTest { // Fix callback and change file to new content shouldFail.set(0); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nFIXED_CERT\n-----END CERTIFICATE-----\n"); - Thread.sleep(100); + TimeUnit.MILLISECONDS.sleep(100); // Should detect new content, reset counter, and succeed ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); From 22d9506206b8b588fe342f3a38dcd81faa590c39 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 25 Mar 2026 16:28:18 +0200 Subject: [PATCH 07/25] Improve SSL certificate reload: rollback safety, defaults, and test encapsulation --- .../src/main/resources/thingsboard.yml | 2 +- .../coapserver/DefaultCoapServerService.java | 10 +- .../CoapDtlsCertificateReloadTest.java | 120 ++++++++++-------- .../LwM2MTransportBootstrapService.java | 17 ++- .../server/DefaultLwM2mTransportService.java | 38 +++--- .../LwM2mBootstrapCertificateReloadTest.java | 21 ++- .../LwM2mServerCertificateReloadTest.java | 3 +- .../service/CertificateReloadManager.java | 4 +- .../src/main/resources/tb-coap-transport.yml | 2 +- .../src/main/resources/tb-http-transport.yml | 2 +- .../src/main/resources/tb-lwm2m-transport.yml | 2 +- .../src/main/resources/tb-mqtt-transport.yml | 2 +- 12 files changed, 120 insertions(+), 103 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 3001ea4187..e4bd13cdb3 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1400,7 +1400,7 @@ transport: # X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.) reload: # Enable/disable automatic SSL certificates reload - enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" + enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:false}" # Check interval in seconds for certificates reload check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}" diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java index 34e7e2cfde..087f8137c7 100644 --- a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java @@ -154,14 +154,14 @@ public class DefaultCoapServerService implements CoapServerService, SmartInitial return networkConfig; } - DtlsConnectorConfig buildDtlsConnectorConfig(Configuration networkConfig) throws UnknownHostException { + private DtlsConnectorConfig buildDtlsConnectorConfig(Configuration networkConfig) throws UnknownHostException { TbCoapDtlsSettings dtlsSettings = coapServerContext.getDtlsSettings(); DtlsConnectorConfig dtlsConnectorConfig = dtlsSettings.dtlsConnectorConfig(networkConfig); networkConfig.set(CoapConfig.COAP_SECURE_PORT, dtlsConnectorConfig.getAddress().getPort()); return dtlsConnectorConfig; } - CoapEndpoint buildDtlsEndpoint(Configuration networkConfig, DTLSConnector connector) { + private CoapEndpoint buildDtlsEndpoint(Configuration networkConfig, DTLSConnector connector) { CoapEndpoint.Builder dtlsCoapEndpointBuilder = new CoapEndpoint.Builder(); dtlsCoapEndpointBuilder.setConfiguration(networkConfig); dtlsCoapEndpointBuilder.setConnector(connector); @@ -179,7 +179,7 @@ public class DefaultCoapServerService implements CoapServerService, SmartInitial tbDtlsCertificateVerifier = (TbCoapDtlsCertificateVerifier) dtlsConnectorConfig.getAdvancedCertificateVerifier(); } - DTLSConnector createDtlsConnector(DtlsConnectorConfig config) { + private DTLSConnector createDtlsConnector(DtlsConnectorConfig config) { return new DTLSConnector(config); } @@ -195,7 +195,7 @@ public class DefaultCoapServerService implements CoapServerService, SmartInitial DTLSConnector newConnector = createDtlsConnector(dtlsConnectorConfig); CoapEndpoint newEndpoint = buildDtlsEndpoint(networkConfig, newConnector); - // Stop old endpoint first to release the port before starting the new one + // Stop the old endpoint first to release the port before starting the new one if (oldDtlsEndpoint != null) { log.info("Stopping old DTLS endpoint to release the port..."); server.getEndpoints().remove(oldDtlsEndpoint); @@ -229,7 +229,7 @@ public class DefaultCoapServerService implements CoapServerService, SmartInitial dtlsCoapEndpoint = newEndpoint; tbDtlsCertificateVerifier = (TbCoapDtlsCertificateVerifier) dtlsConnectorConfig.getAdvancedCertificateVerifier(); - // Destroy old resources after successful swap + // Destroy old resources after a successful swap if (oldDtlsEndpoint != null) { if (oldDtlsConnector != null) { oldDtlsConnector.destroy(); diff --git a/common/coap-server/src/test/java/org/thingsboard/server/coapserver/CoapDtlsCertificateReloadTest.java b/common/coap-server/src/test/java/org/thingsboard/server/coapserver/CoapDtlsCertificateReloadTest.java index a7413bdadd..f22aaf5c7e 100644 --- a/common/coap-server/src/test/java/org/thingsboard/server/coapserver/CoapDtlsCertificateReloadTest.java +++ b/common/coap-server/src/test/java/org/thingsboard/server/coapserver/CoapDtlsCertificateReloadTest.java @@ -18,7 +18,6 @@ package org.thingsboard.server.coapserver; import org.eclipse.californium.core.CoapServer; import org.eclipse.californium.core.network.CoapEndpoint; import org.eclipse.californium.core.network.Endpoint; -import org.eclipse.californium.elements.config.Configuration; import org.eclipse.californium.scandium.DTLSConnector; import org.eclipse.californium.scandium.config.DtlsConnectorConfig; import org.junit.jupiter.api.BeforeEach; @@ -26,22 +25,23 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; -import org.mockito.Mockito; +import org.mockito.MockedConstruction; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.springframework.test.util.ReflectionTestUtils; import java.io.IOException; +import java.net.InetSocketAddress; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockConstruction; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -155,37 +155,42 @@ public class CoapDtlsCertificateReloadTest { // GIVEN when(mockCoapServerContext.getDtlsSettings()).thenReturn(mockDtlsSettings); - CoapEndpoint mockNewEndpoint = mock(CoapEndpoint.class); - DTLSConnector mockNewConnector = mock(DTLSConnector.class); DtlsConnectorConfig mockDtlsConfig = mock(DtlsConnectorConfig.class); TbCoapDtlsCertificateVerifier mockNewVerifier = mock(TbCoapDtlsCertificateVerifier.class); when(mockDtlsConfig.getAdvancedCertificateVerifier()).thenReturn(mockNewVerifier); + when(mockDtlsConfig.getAddress()).thenReturn(new InetSocketAddress("localhost", 5684)); + when(mockDtlsSettings.dtlsConnectorConfig(any())).thenReturn(mockDtlsConfig); - DefaultCoapServerService spyService = Mockito.spy(coapServerService); - ReflectionTestUtils.setField(spyService, "coapServerContext", mockCoapServerContext); - ReflectionTestUtils.setField(spyService, "server", mockCoapServer); - ReflectionTestUtils.setField(spyService, "dtlsCoapEndpoint", mockDtlsEndpoint); - ReflectionTestUtils.setField(spyService, "dtlsConnector", mockDtlsConnector); - - doReturn(mockDtlsConfig).when(spyService).buildDtlsConnectorConfig(any(Configuration.class)); - doReturn(mockNewConnector).when(spyService).createDtlsConnector(any(DtlsConnectorConfig.class)); - doReturn(mockNewEndpoint).when(spyService).buildDtlsEndpoint(any(Configuration.class), any(DTLSConnector.class)); + ReflectionTestUtils.setField(coapServerService, "server", mockCoapServer); + ReflectionTestUtils.setField(coapServerService, "dtlsCoapEndpoint", mockDtlsEndpoint); + ReflectionTestUtils.setField(coapServerService, "dtlsConnector", mockDtlsConnector); List endpointsList = new CopyOnWriteArrayList<>(); endpointsList.add(mockDtlsEndpoint); when(mockCoapServer.getEndpoints()).thenReturn(endpointsList); - // WHEN - ReflectionTestUtils.invokeMethod(spyService, "recreateDtlsEndpoint"); - - // THEN - assertThat(endpointsList).doesNotContain(mockDtlsEndpoint); - verify(mockDtlsEndpoint).stop(); - verify(mockDtlsEndpoint).destroy(); - verify(mockDtlsConnector).destroy(); - verify(mockCoapServer).addEndpoint(mockNewEndpoint); - verify(mockNewEndpoint).start(); - assertThat(ReflectionTestUtils.getField(spyService, "dtlsCoapEndpoint")).isSameAs(mockNewEndpoint); + CoapEndpoint mockNewEndpoint = mock(CoapEndpoint.class); + + try (MockedConstruction dtlsMock = mockConstruction(DTLSConnector.class); + MockedConstruction builderMock = mockConstruction(CoapEndpoint.Builder.class, + (builder, context) -> { + when(builder.build()).thenReturn(mockNewEndpoint); + when(builder.setConfiguration(any())).thenReturn(builder); + when(builder.setConnector(any(DTLSConnector.class))).thenReturn(builder); + })) { + + // WHEN + ReflectionTestUtils.invokeMethod(coapServerService, "recreateDtlsEndpoint"); + + // THEN + assertThat(endpointsList).doesNotContain(mockDtlsEndpoint); + verify(mockDtlsEndpoint).stop(); + verify(mockDtlsEndpoint).destroy(); + verify(mockDtlsConnector).destroy(); + verify(mockCoapServer).addEndpoint(mockNewEndpoint); + verify(mockNewEndpoint).start(); + assertThat(ReflectionTestUtils.getField(coapServerService, "dtlsCoapEndpoint")).isSameAs(mockNewEndpoint); + } } @Test @@ -193,44 +198,49 @@ public class CoapDtlsCertificateReloadTest { // GIVEN when(mockCoapServerContext.getDtlsSettings()).thenReturn(mockDtlsSettings); - CoapEndpoint mockNewEndpoint = mock(CoapEndpoint.class); - DTLSConnector mockNewConnector = mock(DTLSConnector.class); DtlsConnectorConfig mockDtlsConfig = mock(DtlsConnectorConfig.class); + when(mockDtlsConfig.getAddress()).thenReturn(new InetSocketAddress("localhost", 5684)); + when(mockDtlsSettings.dtlsConnectorConfig(any())).thenReturn(mockDtlsConfig); - doThrow(new IOException("start failed")).when(mockNewEndpoint).start(); - - DefaultCoapServerService spyService = Mockito.spy(coapServerService); - ReflectionTestUtils.setField(spyService, "coapServerContext", mockCoapServerContext); - ReflectionTestUtils.setField(spyService, "server", mockCoapServer); - ReflectionTestUtils.setField(spyService, "dtlsCoapEndpoint", mockDtlsEndpoint); - ReflectionTestUtils.setField(spyService, "dtlsConnector", mockDtlsConnector); - - doReturn(mockDtlsConfig).when(spyService).buildDtlsConnectorConfig(any(Configuration.class)); - doReturn(mockNewConnector).when(spyService).createDtlsConnector(any(DtlsConnectorConfig.class)); - doReturn(mockNewEndpoint).when(spyService).buildDtlsEndpoint(any(Configuration.class), any(DTLSConnector.class)); + ReflectionTestUtils.setField(coapServerService, "server", mockCoapServer); + ReflectionTestUtils.setField(coapServerService, "dtlsCoapEndpoint", mockDtlsEndpoint); + ReflectionTestUtils.setField(coapServerService, "dtlsConnector", mockDtlsConnector); List endpointsList = new CopyOnWriteArrayList<>(); endpointsList.add(mockDtlsEndpoint); when(mockCoapServer.getEndpoints()).thenReturn(endpointsList); - // WHEN - the callback catches the IOException internally - spyService.afterSingletonsInstantiated(); - - ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); - verify(mockDtlsSettings).registerReloadCallback(callbackCaptor.capture()); - Runnable reloadCallback = callbackCaptor.getValue(); - reloadCallback.run(); + CoapEndpoint mockNewEndpoint = mock(CoapEndpoint.class); + doThrow(new IOException("start failed")).when(mockNewEndpoint).start(); - // THEN - new resources cleaned up - verify(mockNewEndpoint).destroy(); - verify(mockNewConnector).destroy(); - assertThat(endpointsList).doesNotContain(mockNewEndpoint); - // Old endpoint was stopped to release port, then restored after new one failed - verify(mockDtlsEndpoint).stop(); - verify(mockDtlsEndpoint).start(); - // Old fields preserved - assertThat(ReflectionTestUtils.getField(spyService, "dtlsCoapEndpoint")).isSameAs(mockDtlsEndpoint); - assertThat(ReflectionTestUtils.getField(spyService, "dtlsConnector")).isSameAs(mockDtlsConnector); + try (MockedConstruction dtlsMock = mockConstruction(DTLSConnector.class); + MockedConstruction builderMock = mockConstruction(CoapEndpoint.Builder.class, + (builder, context) -> { + when(builder.build()).thenReturn(mockNewEndpoint); + when(builder.setConfiguration(any())).thenReturn(builder); + when(builder.setConnector(any(DTLSConnector.class))).thenReturn(builder); + })) { + + // WHEN + coapServerService.afterSingletonsInstantiated(); + + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mockDtlsSettings).registerReloadCallback(callbackCaptor.capture()); + Runnable reloadCallback = callbackCaptor.getValue(); + reloadCallback.run(); + + // THEN - new resources cleaned up + DTLSConnector constructedConnector = dtlsMock.constructed().get(0); + verify(mockNewEndpoint).destroy(); + verify(constructedConnector).destroy(); + assertThat(endpointsList).doesNotContain(mockNewEndpoint); + // Old endpoint was stopped to release port, then restored after new one failed + verify(mockDtlsEndpoint).stop(); + verify(mockDtlsEndpoint).start(); + // Old fields preserved + assertThat(ReflectionTestUtils.getField(coapServerService, "dtlsCoapEndpoint")).isSameAs(mockDtlsEndpoint); + assertThat(ReflectionTestUtils.getField(coapServerService, "dtlsConnector")).isSameAs(mockDtlsConnector); + } } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java index a9a4e364b5..9b370d0b71 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java @@ -185,10 +185,10 @@ public class LwM2MTransportBootstrapService implements SmartInitializingSingleto log.info("Creating new LwM2M Bootstrap server with updated certificates..."); LeshanBootstrapServer newServer = getLhBootstrapServer(); - // Stop the old server first to release the ports before starting the new one + // Stop (not destroy) the old server to release ports but keep it restartable for rollback if (oldServer != null) { log.info("Stopping old LwM2M Bootstrap server to release ports..."); - oldServer.destroy(); + oldServer.stop(); } try { @@ -196,13 +196,11 @@ public class LwM2MTransportBootstrapService implements SmartInitializingSingleto } catch (Exception e) { log.error("Failed to start new LwM2M Bootstrap server", e); newServer.destroy(); - // Attempt to restore the old server + // Attempt to restart the old server (only stopped, not destroyed) if (oldServer != null) { try { - LeshanBootstrapServer restoredServer = getLhBootstrapServer(); - restoredServer.start(); - this.server = restoredServer; - log.info("Restored LwM2M Bootstrap server with previous configuration."); + oldServer.start(); + log.info("Restored old LwM2M Bootstrap server successfully."); } catch (Exception restoreEx) { log.error("Failed to restore old LwM2M Bootstrap server", restoreEx); } @@ -211,6 +209,11 @@ public class LwM2MTransportBootstrapService implements SmartInitializingSingleto } this.server = newServer; log.info("New LwM2M Bootstrap server started successfully."); + + // Destroy the old server only after a successful swap + if (oldServer != null) { + oldServer.destroy(); + } } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java index b0d25729b3..5815a47555 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java @@ -236,7 +236,7 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService, Smar log.info("Creating new LwM2M server with updated certificates..."); LeshanServer newServer = getLhServer(); - // Stop old server first to release the ports before starting the new one + // Stop (not destroy) old server to release ports but keep it restartable for rollback if (oldServer != null) { log.info("Stopping old LwM2M server to release ports..."); if (oldListener != null) { @@ -245,7 +245,7 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService, Smar oldServer.getObservationService().removeListener(oldListener.observationListener); oldServer.getSendService().removeListener(oldListener.sendListener); } - oldServer.destroy(); + oldServer.stop(); } try { @@ -253,21 +253,20 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService, Smar } catch (Exception e) { log.error("Failed to start new LwM2M server", e); newServer.destroy(); - // Attempt to restore the old server - try { - LeshanServer restoredServer = getLhServer(); - restoredServer.start(); - LwM2mServerListener restoredListener = new LwM2mServerListener(handler); - restoredServer.getRegistrationService().addListener(restoredListener.registrationListener); - restoredServer.getPresenceService().addListener(restoredListener.presenceListener); - restoredServer.getObservationService().addListener(restoredListener.observationListener); - restoredServer.getSendService().addListener(restoredListener.sendListener); - this.server = restoredServer; - this.context.setServer(restoredServer); - this.serverListener = restoredListener; - log.info("Restored LwM2M server with previous configuration."); - } catch (Exception restoreEx) { - log.error("Failed to restore old LwM2M server", restoreEx); + // Attempt to restart the old server (only stopped, not destroyed) + if (oldServer != null) { + try { + oldServer.start(); + if (oldListener != null) { + oldServer.getRegistrationService().addListener(oldListener.registrationListener); + oldServer.getPresenceService().addListener(oldListener.presenceListener); + oldServer.getObservationService().addListener(oldListener.observationListener); + oldServer.getSendService().addListener(oldListener.sendListener); + } + log.info("Restored old LwM2M server successfully."); + } catch (Exception restoreEx) { + log.error("Failed to restore old LwM2M server", restoreEx); + } } throw e; } @@ -282,6 +281,11 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService, Smar this.context.setServer(newServer); this.serverListener = newListener; log.info("New LwM2M server started successfully."); + + // Destroy old server only after successful swap + if (oldServer != null) { + oldServer.destroy(); + } } @Override diff --git a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2mBootstrapCertificateReloadTest.java b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2mBootstrapCertificateReloadTest.java index 84bc44b03b..bbcbc921f6 100644 --- a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2mBootstrapCertificateReloadTest.java +++ b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2mBootstrapCertificateReloadTest.java @@ -122,6 +122,7 @@ public class LwM2mBootstrapCertificateReloadTest { // The old server should NOT be destroyed since the new server was never created. reloadCallback.run(); + verify(mockBootstrapServer, never()).stop(); verify(mockBootstrapServer, never()).destroy(); assertThat(ReflectionTestUtils.getField(bootstrapService, "server")).isSameAs(mockBootstrapServer); } @@ -164,18 +165,15 @@ public class LwM2mBootstrapCertificateReloadTest { } @Test - public void givenReloadCallback_whenNewServerStartFails_thenNewServerDestroyedAndRestorationAttempted() { + public void givenReloadCallback_whenNewServerStartFails_thenOldServerRestarted() { // GIVEN ReflectionTestUtils.setField(bootstrapService, "server", mockBootstrapServer); LeshanBootstrapServer mockNewServer = mock(LeshanBootstrapServer.class); doThrow(new RuntimeException("start failed")).when(mockNewServer).start(); - LeshanBootstrapServer mockRestoredServer = mock(LeshanBootstrapServer.class); - LwM2MTransportBootstrapService spyService = Mockito.spy(bootstrapService); - // First call returns the failing server, second call returns the restoration server - doReturn(mockNewServer).doReturn(mockRestoredServer).when(spyService).getLhBootstrapServer(); + doReturn(mockNewServer).when(spyService).getLhBootstrapServer(); ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); spyService.afterSingletonsInstantiated(); @@ -187,13 +185,14 @@ public class LwM2mBootstrapCertificateReloadTest { reloadCallback.run(); // THEN - // Old server is destroyed to release ports - verify(mockBootstrapServer).destroy(); - // New server fails to start and is destroyed + // Old server is stopped (not destroyed) to release ports + verify(mockBootstrapServer).stop(); + verify(mockBootstrapServer, never()).destroy(); + // The new server fails to start and is destroyed verify(mockNewServer).destroy(); - // Restoration server is started and becomes the active server - verify(mockRestoredServer).start(); - assertThat(ReflectionTestUtils.getField(spyService, "server")).isSameAs(mockRestoredServer); + // Old server is restarted (not rebuilt from potentially stale credentials) + verify(mockBootstrapServer).start(); + assertThat(ReflectionTestUtils.getField(spyService, "server")).isSameAs(mockBootstrapServer); } } diff --git a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerCertificateReloadTest.java b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerCertificateReloadTest.java index c8b7f0d060..93b74447fd 100644 --- a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerCertificateReloadTest.java +++ b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerCertificateReloadTest.java @@ -139,9 +139,10 @@ public class LwM2mServerCertificateReloadTest { // Force getLhServer() to fail by returning null host (causes InetSocketAddress to throw) when(mockConfig.getHost()).thenReturn(null); - // With create-then-swap, the old server should NOT be destroyed if the new one fails. + // With create-then-swap, the old server should NOT be stopped/destroyed if the new one fails to build. reloadCallback.run(); + verify(mockLeshanServer, never()).stop(); verify(mockLeshanServer, never()).destroy(); // Old server should still be the active one assertThat(ReflectionTestUtils.getField(lwm2mTransportService, "server")).isSameAs(mockLeshanServer); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java index 0dd1fae724..f243c64c50 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java @@ -48,7 +48,7 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis private static final int MAX_CONSECUTIVE_FAILURES = 10; - @Value("${transport.ssl.certificate.reload.enabled:true}") + @Value("${transport.ssl.certificate.reload.enabled:false}") private boolean reloadEnabled; @Value("${transport.ssl.certificate.reload.check_interval_seconds:60}") @@ -103,7 +103,7 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis List filePaths = credentials.getCertificateFilePaths(); if (filePaths == null || filePaths.isEmpty()) { - log.debug("No certificate files to watch for: {} ({})", config.getName(), beanName); + log.debug("No file-system certificate paths to watch for: {} ({}) — certificates may be classpath-based", config.getName(), beanName); continue; } diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index 3c1ef94f09..f9827d488f 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -176,7 +176,7 @@ transport: # X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.) reload: # Enable/disable automatic SSL certificates reload - enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" + enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:false}" # Check interval in seconds for certificates reload check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}" diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index 1b221d1fd9..7efa428a90 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -207,7 +207,7 @@ transport: # X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.) reload: # Enable/disable automatic SSL certificates reload - enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" + enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:false}" # Check interval in seconds for certificates reload check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}" diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 6140122062..543b11859b 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -307,7 +307,7 @@ transport: # X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.) reload: # Enable/disable automatic SSL certificates reload - enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" + enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:false}" # Check interval in seconds for certificates reload check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}" diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index ac02fa396b..60d3da8f8e 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -240,7 +240,7 @@ transport: # X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.) reload: # Enable/disable automatic SSL certificates reload - enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" + enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:false}" # Check interval in seconds for certificates reload check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}" From 635920534d3efe5d25816eb987d604417b5b6d13 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 25 Mar 2026 16:37:40 +0200 Subject: [PATCH 08/25] Improve SSL reload clarity: document trade-offs, use Path API, deduplicate PEM path logic --- .../coapserver/DefaultCoapServerService.java | 5 +++- .../config/ssl/KeystoreSslCredentials.java | 8 +++---- .../config/ssl/PemSslCredentials.java | 24 ++++++++----------- .../service/CertificateReloadManager.java | 2 ++ 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java index 087f8137c7..03de8b7b91 100644 --- a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java @@ -154,6 +154,7 @@ public class DefaultCoapServerService implements CoapServerService, SmartInitial return networkConfig; } + // Note: this method has a side effect — it sets COAP_SECURE_PORT on the provided networkConfig. private DtlsConnectorConfig buildDtlsConnectorConfig(Configuration networkConfig) throws UnknownHostException { TbCoapDtlsSettings dtlsSettings = coapServerContext.getDtlsSettings(); DtlsConnectorConfig dtlsConnectorConfig = dtlsSettings.dtlsConnectorConfig(networkConfig); @@ -195,7 +196,9 @@ public class DefaultCoapServerService implements CoapServerService, SmartInitial DTLSConnector newConnector = createDtlsConnector(dtlsConnectorConfig); CoapEndpoint newEndpoint = buildDtlsEndpoint(networkConfig, newConnector); - // Stop the old endpoint first to release the port before starting the new one + // Californium binds the DTLS port at connector construction time, so we must stop the old + // endpoint first to release the port. This creates a brief window where the port is unbound; + // if the new endpoint fails to start, we attempt to restore the old one (see rollback below). if (oldDtlsEndpoint != null) { log.info("Stopping old DTLS endpoint to release the port..."); server.getEndpoints().remove(oldDtlsEndpoint); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/KeystoreSslCredentials.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/KeystoreSslCredentials.java index 7a2fb1a545..7cbc4403b7 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/KeystoreSslCredentials.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/KeystoreSslCredentials.java @@ -20,9 +20,9 @@ import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.ResourceUtils; import org.thingsboard.server.common.data.StringUtils; -import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; import java.nio.file.Path; import java.security.GeneralSecurityException; import java.security.KeyStore; @@ -62,9 +62,9 @@ public class KeystoreSslCredentials extends AbstractSslCredentials { @Override public List getCertificateFilePaths() { if (!StringUtils.isEmpty(storeFile) && !storeFile.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) { - File storeFileObj = new File(storeFile); - if (storeFileObj.exists()) { - return Collections.singletonList(storeFileObj.toPath().toAbsolutePath()); + Path resolved = Path.of(storeFile).toAbsolutePath(); + if (Files.exists(resolved)) { + return Collections.singletonList(resolved); } } return Collections.emptyList(); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/PemSslCredentials.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/PemSslCredentials.java index 72ad7af9c5..c6eb75698e 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/PemSslCredentials.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/PemSslCredentials.java @@ -30,10 +30,10 @@ import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; import org.thingsboard.server.common.data.ResourceUtils; import org.thingsboard.server.common.data.StringUtils; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.nio.file.Files; import java.nio.file.Path; import java.security.GeneralSecurityException; import java.security.KeyStore; @@ -145,22 +145,18 @@ public class PemSslCredentials extends AbstractSslCredentials { @Override public List getCertificateFilePaths() { List paths = new ArrayList<>(); + addIfFileSystemPath(paths, certFile); + addIfFileSystemPath(paths, keyFile); + return paths; + } - if (!StringUtils.isEmpty(certFile) && !certFile.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) { - File certFileObj = new File(certFile); - if (certFileObj.exists()) { - paths.add(certFileObj.toPath().toAbsolutePath()); - } - } - - if (!StringUtils.isEmpty(keyFile) && !keyFile.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) { - File keyFileObj = new File(keyFile); - if (keyFileObj.exists()) { - paths.add(keyFileObj.toPath().toAbsolutePath()); + private static void addIfFileSystemPath(List paths, String filePath) { + if (!StringUtils.isEmpty(filePath) && !filePath.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) { + Path resolved = Path.of(filePath).toAbsolutePath(); + if (Files.exists(resolved)) { + paths.add(resolved); } } - - return paths; } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java index f243c64c50..eec84a22f0 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java @@ -247,6 +247,8 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis } catch (Exception e) { consecutiveFailures++; failedCombinedChecksum = combinedChecksum; + // Deliberately NOT updating the lastModifiedMap here, so the next poll cycle retries + // (mtime mismatch passes the early gate, checksum matches failedCombinedChecksum). log.error("Failed to reload certificate for {} (attempt {}/{}): {}", name, consecutiveFailures, MAX_CONSECUTIVE_FAILURES, e.getMessage(), e); } From 1317a446390f5b31e570a1383622709a07f0f677 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 26 Mar 2026 10:18:11 +0200 Subject: [PATCH 09/25] Add reload integration tests --- ...pDtlsCertificateReloadIntegrationTest.java | 349 ++++++++++++++++++ ...ttSslCertificateReloadIntegrationTest.java | 276 ++++++++++++++ 2 files changed, 625 insertions(+) create mode 100644 common/coap-server/src/test/java/org/thingsboard/server/coapserver/CoapDtlsCertificateReloadIntegrationTest.java create mode 100644 common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttSslCertificateReloadIntegrationTest.java diff --git a/common/coap-server/src/test/java/org/thingsboard/server/coapserver/CoapDtlsCertificateReloadIntegrationTest.java b/common/coap-server/src/test/java/org/thingsboard/server/coapserver/CoapDtlsCertificateReloadIntegrationTest.java new file mode 100644 index 0000000000..11e278b4f5 --- /dev/null +++ b/common/coap-server/src/test/java/org/thingsboard/server/coapserver/CoapDtlsCertificateReloadIntegrationTest.java @@ -0,0 +1,349 @@ +/** + * 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.coapserver; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemWriter; +import org.eclipse.californium.core.CoapClient; +import org.eclipse.californium.core.CoapResource; +import org.eclipse.californium.core.CoapResponse; +import org.eclipse.californium.core.CoapServer; +import org.eclipse.californium.core.coap.CoAP; +import org.eclipse.californium.core.config.CoapConfig; +import org.eclipse.californium.core.network.CoapEndpoint; +import org.eclipse.californium.core.server.resources.CoapExchange; +import org.eclipse.californium.elements.config.Configuration; +import org.eclipse.californium.elements.util.SslContextUtil; +import org.eclipse.californium.scandium.DTLSConnector; +import org.eclipse.californium.scandium.config.DtlsConfig; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.eclipse.californium.scandium.dtls.CertificateType; +import org.eclipse.californium.scandium.dtls.x509.SingleCertificateProvider; +import org.eclipse.californium.scandium.dtls.x509.StaticNewAdvancedCertificateVerifier; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.thingsboard.server.common.transport.config.ssl.KeystoreSslCredentials; +import org.thingsboard.server.common.transport.config.ssl.PemSslCredentials; +import org.thingsboard.server.common.transport.config.ssl.SslCredentials; +import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig; +import org.thingsboard.server.common.transport.config.ssl.SslCredentialsType; + +import java.io.OutputStreamWriter; +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CLIENT_AUTHENTICATION_MODE; +import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RETRANSMISSION_TIMEOUT; +import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_ROLE; +import static org.eclipse.californium.scandium.config.DtlsConfig.DtlsRole.SERVER_ONLY; + +public class CoapDtlsCertificateReloadIntegrationTest { + + private static final String TEST_RESOURCE_PATH = "test"; + private static final String TEST_PAYLOAD = "hello-dtls"; + + @TempDir + Path tempDir; + + private CoapServer coapServer; + + @AfterEach + public void teardown() { + if (coapServer != null) { + coapServer.destroy(); + } + } + + @Test + public void givenDtlsServer_whenCertFileChangedAndReloadTriggered_thenNewEndpointServesNewCert() throws Exception { + KeyPair keyPairA = generateKeyPair(); + X509Certificate certA = generateSelfSignedCert(keyPairA, "CN=ServerA"); + KeyPair keyPairB = generateKeyPair(); + X509Certificate certB = generateSelfSignedCert(keyPairB, "CN=ServerB"); + + Path certFile = tempDir.resolve("server-cert.pem"); + Path keyFile = tempDir.resolve("server-key.pem"); + writeCertPem(certFile, certA); + writeKeyPem(keyFile, keyPairA); + + SslCredentialsConfig credentialsConfig = createSslCredentialsConfig(certFile, keyFile); + + Configuration config = createServerConfig(); + coapServer = new CoapServer(config); + coapServer.add(new TestResource()); + + int dtlsPort = findAvailablePort(); + CoapEndpoint endpointA = buildDtlsEndpointFromCredentials(config, credentialsConfig.getCredentials(), dtlsPort); + coapServer.addEndpoint(endpointA); + coapServer.start(); + + CoapResponse responseA = doDtlsRequest(dtlsPort, certA); + assertThat(responseA).isNotNull(); + assertThat(responseA.getCode()).isEqualTo(CoAP.ResponseCode.CONTENT); + assertThat(responseA.getResponseText()).isEqualTo(TEST_PAYLOAD); + + writeCertPem(certFile, certB); + writeKeyPem(keyFile, keyPairB); + credentialsConfig.onCertificateFileChanged(); + + coapServer.getEndpoints().remove(endpointA); + endpointA.stop(); + + CoapEndpoint endpointB = buildDtlsEndpointFromCredentials(config, credentialsConfig.getCredentials(), dtlsPort); + coapServer.addEndpoint(endpointB); + endpointB.start(); + endpointA.destroy(); + + CoapResponse responseB = doDtlsRequest(dtlsPort, certB); + assertThat(responseB).isNotNull(); + assertThat(responseB.getCode()).isEqualTo(CoAP.ResponseCode.CONTENT); + assertThat(responseB.getResponseText()).isEqualTo(TEST_PAYLOAD); + } + + @Test + public void givenDtlsServer_whenCertReloaded_thenOldCertClientFails() throws Exception { + KeyPair keyPairA = generateKeyPair(); + X509Certificate certA = generateSelfSignedCert(keyPairA, "CN=ServerA"); + KeyPair keyPairB = generateKeyPair(); + X509Certificate certB = generateSelfSignedCert(keyPairB, "CN=ServerB"); + + Path certFile = tempDir.resolve("server-cert.pem"); + Path keyFile = tempDir.resolve("server-key.pem"); + writeCertPem(certFile, certA); + writeKeyPem(keyFile, keyPairA); + + SslCredentialsConfig credentialsConfig = createSslCredentialsConfig(certFile, keyFile); + + Configuration config = createServerConfig(); + coapServer = new CoapServer(config); + coapServer.add(new TestResource()); + + int dtlsPort = findAvailablePort(); + CoapEndpoint endpointA = buildDtlsEndpointFromCredentials(config, credentialsConfig.getCredentials(), dtlsPort); + coapServer.addEndpoint(endpointA); + coapServer.start(); + + CoapResponse responseA = doDtlsRequest(dtlsPort, certA); + assertThat(responseA).isNotNull(); + + writeCertPem(certFile, certB); + writeKeyPem(keyFile, keyPairB); + credentialsConfig.onCertificateFileChanged(); + + coapServer.getEndpoints().remove(endpointA); + endpointA.stop(); + CoapEndpoint endpointB = buildDtlsEndpointFromCredentials(config, credentialsConfig.getCredentials(), dtlsPort); + coapServer.addEndpoint(endpointB); + endpointB.start(); + endpointA.destroy(); + + CoapResponse failedResponse = doDtlsRequest(dtlsPort, certA); + assertThat(failedResponse).isNull(); + + CoapResponse responseB = doDtlsRequest(dtlsPort, certB); + assertThat(responseB).isNotNull(); + assertThat(responseB.getCode()).isEqualTo(CoAP.ResponseCode.CONTENT); + } + + @Test + public void givenDtlsServer_whenReloadWithSameCert_thenConnectionStillWorks() throws Exception { + KeyPair keyPair = generateKeyPair(); + X509Certificate cert = generateSelfSignedCert(keyPair, "CN=Server"); + + Path certFile = tempDir.resolve("server-cert.pem"); + Path keyFile = tempDir.resolve("server-key.pem"); + writeCertPem(certFile, cert); + writeKeyPem(keyFile, keyPair); + + SslCredentialsConfig credentialsConfig = createSslCredentialsConfig(certFile, keyFile); + + Configuration config = createServerConfig(); + coapServer = new CoapServer(config); + coapServer.add(new TestResource()); + + int dtlsPort = findAvailablePort(); + CoapEndpoint endpoint1 = buildDtlsEndpointFromCredentials(config, credentialsConfig.getCredentials(), dtlsPort); + coapServer.addEndpoint(endpoint1); + coapServer.start(); + + CoapResponse response1 = doDtlsRequest(dtlsPort, cert); + assertThat(response1).isNotNull(); + assertThat(response1.getCode()).isEqualTo(CoAP.ResponseCode.CONTENT); + + credentialsConfig.onCertificateFileChanged(); + + coapServer.getEndpoints().remove(endpoint1); + endpoint1.stop(); + CoapEndpoint endpoint2 = buildDtlsEndpointFromCredentials(config, credentialsConfig.getCredentials(), dtlsPort); + coapServer.addEndpoint(endpoint2); + endpoint2.start(); + endpoint1.destroy(); + + CoapResponse response2 = doDtlsRequest(dtlsPort, cert); + assertThat(response2).isNotNull(); + assertThat(response2.getCode()).isEqualTo(CoAP.ResponseCode.CONTENT); + } + + private SslCredentialsConfig createSslCredentialsConfig(Path certFile, Path keyFile) { + PemSslCredentials pem = new PemSslCredentials(); + pem.setCertFile(certFile.toAbsolutePath().toString()); + pem.setKeyFile(keyFile.toAbsolutePath().toString()); + + SslCredentialsConfig config = new SslCredentialsConfig("CoAP DTLS Test", false); + config.setEnabled(true); + config.setType(SslCredentialsType.PEM); + config.setPem(pem); + config.setKeystore(new KeystoreSslCredentials()); + config.init(); + return config; + } + + private CoapEndpoint buildDtlsEndpointFromCredentials(Configuration config, SslCredentials credentials, int port) { + DtlsConnectorConfig.Builder dtlsBuilder = new DtlsConnectorConfig.Builder(config); + dtlsBuilder.setAddress(new InetSocketAddress(InetAddress.getLoopbackAddress(), port)); + dtlsBuilder.set(DTLS_ROLE, SERVER_ONLY); + dtlsBuilder.set(DTLS_RETRANSMISSION_TIMEOUT, 3000, MILLISECONDS); + dtlsBuilder.set(DTLS_CLIENT_AUTHENTICATION_MODE, + org.eclipse.californium.elements.config.CertificateAuthenticationMode.WANTED); + + SslContextUtil.Credentials serverCreds = new SslContextUtil.Credentials( + credentials.getPrivateKey(), null, credentials.getCertificateChain()); + + dtlsBuilder.setCertificateIdentityProvider( + new SingleCertificateProvider(serverCreds.getPrivateKey(), serverCreds.getCertificateChain(), + Collections.singletonList(CertificateType.X_509))); + + dtlsBuilder.setAdvancedCertificateVerifier( + StaticNewAdvancedCertificateVerifier.builder() + .setTrustAllCertificates() + .build()); + + DTLSConnector connector = new DTLSConnector(dtlsBuilder.build()); + + CoapEndpoint.Builder endpointBuilder = new CoapEndpoint.Builder(); + endpointBuilder.setConfiguration(config); + endpointBuilder.setConnector(connector); + return endpointBuilder.build(); + } + + private KeyPair generateKeyPair() throws Exception { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); + kpg.initialize(256); + return kpg.generateKeyPair(); + } + + private X509Certificate generateSelfSignedCert(KeyPair kp, String subjectDn) throws Exception { + X500Name subject = new X500Name(subjectDn); + Date now = new Date(); + Date expiry = new Date(now.getTime() + TimeUnit.DAYS.toMillis(1)); + return new JcaX509CertificateConverter().getCertificate( + new JcaX509v3CertificateBuilder( + subject, BigInteger.valueOf(System.nanoTime()), now, expiry, + subject, kp.getPublic()) + .build(new JcaContentSignerBuilder("SHA256withECDSA").build(kp.getPrivate()))); + } + + private void writeCertPem(Path path, X509Certificate cert) throws Exception { + try (PemWriter writer = new PemWriter(new OutputStreamWriter(Files.newOutputStream(path)))) { + writer.writeObject(new PemObject("CERTIFICATE", cert.getEncoded())); + } + } + + private void writeKeyPem(Path path, KeyPair keyPair) throws Exception { + try (PemWriter writer = new PemWriter(new OutputStreamWriter(Files.newOutputStream(path)))) { + writer.writeObject(new PemObject("PRIVATE KEY", keyPair.getPrivate().getEncoded())); + } + } + + private Configuration createServerConfig() { + Configuration config = new Configuration(); + config.set(CoapConfig.MAX_RETRANSMIT, 2); + config.set(CoapConfig.RESPONSE_MATCHING, CoapConfig.MatcherMode.RELAXED); + return config; + } + + private CoapResponse doDtlsRequest(int port, X509Certificate trustedCert) { + try { + Configuration clientConfig = new Configuration(); + clientConfig.set(CoapConfig.MAX_RETRANSMIT, 1); + clientConfig.set(DtlsConfig.DTLS_ROLE, DtlsConfig.DtlsRole.CLIENT_ONLY); + clientConfig.set(DtlsConfig.DTLS_RETRANSMISSION_TIMEOUT, 2000, MILLISECONDS); + clientConfig.set(DtlsConfig.DTLS_USE_HELLO_VERIFY_REQUEST, false); + clientConfig.set(DtlsConfig.DTLS_VERIFY_SERVER_CERTIFICATES_SUBJECT, false); + + DtlsConnectorConfig.Builder clientDtls = new DtlsConnectorConfig.Builder(clientConfig); + clientDtls.setAdvancedCertificateVerifier( + StaticNewAdvancedCertificateVerifier.builder() + .setTrustedCertificates(trustedCert) + .build()); + + DTLSConnector clientConnector = new DTLSConnector(clientDtls.build()); + CoapEndpoint clientEndpoint = new CoapEndpoint.Builder() + .setConfiguration(clientConfig) + .setConnector(clientConnector) + .build(); + + CoapClient client = new CoapClient("coaps://127.0.0.1:" + port + "/" + TEST_RESOURCE_PATH); + client.setEndpoint(clientEndpoint); + client.setTimeout((long) 5000); + + try { + clientEndpoint.start(); + return client.get(); + } finally { + client.shutdown(); + clientEndpoint.destroy(); + } + } catch (Exception e) { + return null; + } + } + + private int findAvailablePort() throws Exception { + try (java.net.DatagramSocket socket = new java.net.DatagramSocket(0)) { + return socket.getLocalPort(); + } + } + + private static class TestResource extends CoapResource { + TestResource() { + super(TEST_RESOURCE_PATH); + } + + @Override + public void handleGET(CoapExchange exchange) { + exchange.respond(CoAP.ResponseCode.CONTENT, TEST_PAYLOAD); + } + + } + +} diff --git a/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttSslCertificateReloadIntegrationTest.java b/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttSslCertificateReloadIntegrationTest.java new file mode 100644 index 0000000000..84ef4f2979 --- /dev/null +++ b/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttSslCertificateReloadIntegrationTest.java @@ -0,0 +1,276 @@ +/** + * 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.transport.mqtt; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemWriter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.common.transport.config.ssl.PemSslCredentials; +import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.io.OutputStreamWriter; +import java.math.BigInteger; +import java.net.InetAddress; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +public class MqttSslCertificateReloadIntegrationTest { + + @TempDir + Path tempDir; + + @Mock + private TransportService transportService; + + @Test + public void givenMqttSslProvider_whenCertFileChangedAndReloadTriggered_thenNewConnectionSeesNewCert() throws Exception { + KeyPair keyPairA = generateKeyPair(); + X509Certificate certA = generateSelfSignedCert(keyPairA, "CN=CertA"); + + KeyPair keyPairB = generateKeyPair(); + X509Certificate certB = generateSelfSignedCert(keyPairB, "CN=CertB"); + + Path certFile = tempDir.resolve("server-cert.pem"); + Path keyFile = tempDir.resolve("server-key.pem"); + writeCertPem(certFile, certA); + writeKeyPem(keyFile, keyPairA); + + SslCredentialsConfig credentialsConfig = createSslCredentialsConfig(certFile, keyFile); + MqttSslHandlerProvider provider = createMqttSslHandlerProvider(credentialsConfig); + + SSLContext ctxA = getProviderSslContext(provider); + X509Certificate servedA; + try (SSLServerSocket ss = createServerSocket(ctxA)) { + servedA = doHandshakeAndGetServerCert(ss); + } + assertThat(servedA.getSubjectX500Principal()).isEqualTo(certA.getSubjectX500Principal()); + + writeCertPem(certFile, certB); + writeKeyPem(keyFile, keyPairB); + + credentialsConfig.onCertificateFileChanged(); + + SSLContext ctxB = getProviderSslContext(provider); + assertThat(ctxB).isNotSameAs(ctxA); + X509Certificate servedB; + try (SSLServerSocket ss = createServerSocket(ctxB)) { + servedB = doHandshakeAndGetServerCert(ss); + } + assertThat(servedB.getSubjectX500Principal()).isEqualTo(certB.getSubjectX500Principal()); + assertThat(servedB.getSubjectX500Principal()).isNotEqualTo(servedA.getSubjectX500Principal()); + } + + @Test + public void givenMqttSslProvider_whenReloadCalledWithSameFiles_thenSslContextIsRecreated() throws Exception { + KeyPair keyPair = generateKeyPair(); + X509Certificate cert = generateSelfSignedCert(keyPair, "CN=SameCert"); + + Path certFile = tempDir.resolve("server-cert.pem"); + Path keyFile = tempDir.resolve("server-key.pem"); + writeCertPem(certFile, cert); + writeKeyPem(keyFile, keyPair); + + SslCredentialsConfig credentialsConfig = createSslCredentialsConfig(certFile, keyFile); + MqttSslHandlerProvider provider = createMqttSslHandlerProvider(credentialsConfig); + + SSLContext ctx1 = getProviderSslContext(provider); + assertThat(ctx1).isNotNull(); + + credentialsConfig.onCertificateFileChanged(); + + SSLContext ctx2 = getProviderSslContext(provider); + assertThat(ctx2).isNotSameAs(ctx1); + + X509Certificate served; + try (SSLServerSocket ss = createServerSocket(ctx2)) { + served = doHandshakeAndGetServerCert(ss); + } + assertThat(served.getSubjectX500Principal()).isEqualTo(cert.getSubjectX500Principal()); + } + + @Test + public void givenMqttSslProvider_whenMultipleReloads_thenEachProducesNewContext() throws Exception { + KeyPair keyPairA = generateKeyPair(); + X509Certificate certA = generateSelfSignedCert(keyPairA, "CN=CertA"); + KeyPair keyPairB = generateKeyPair(); + X509Certificate certB = generateSelfSignedCert(keyPairB, "CN=CertB"); + KeyPair keyPairC = generateKeyPair(); + X509Certificate certC = generateSelfSignedCert(keyPairC, "CN=CertC"); + + Path certFile = tempDir.resolve("server-cert.pem"); + Path keyFile = tempDir.resolve("server-key.pem"); + writeCertPem(certFile, certA); + writeKeyPem(keyFile, keyPairA); + + SslCredentialsConfig credentialsConfig = createSslCredentialsConfig(certFile, keyFile); + MqttSslHandlerProvider provider = createMqttSslHandlerProvider(credentialsConfig); + + SSLContext ctx1 = getProviderSslContext(provider); + + writeCertPem(certFile, certB); + writeKeyPem(keyFile, keyPairB); + credentialsConfig.onCertificateFileChanged(); + SSLContext ctx2 = getProviderSslContext(provider); + + writeCertPem(certFile, certC); + writeKeyPem(keyFile, keyPairC); + credentialsConfig.onCertificateFileChanged(); + SSLContext ctx3 = getProviderSslContext(provider); + + assertThat(ctx1).isNotSameAs(ctx2); + assertThat(ctx2).isNotSameAs(ctx3); + + X509Certificate served; + try (SSLServerSocket ss = createServerSocket(ctx3)) { + served = doHandshakeAndGetServerCert(ss); + } + assertThat(served.getSubjectX500Principal()).isEqualTo(certC.getSubjectX500Principal()); + } + + private SslCredentialsConfig createSslCredentialsConfig(Path certFile, Path keyFile) throws Exception { + PemSslCredentials pem = new PemSslCredentials(); + pem.setCertFile(certFile.toAbsolutePath().toString()); + pem.setKeyFile(keyFile.toAbsolutePath().toString()); + + SslCredentialsConfig config = new SslCredentialsConfig("MQTT SSL Test", false); + config.setEnabled(true); + config.setType(org.thingsboard.server.common.transport.config.ssl.SslCredentialsType.PEM); + config.setPem(pem); + config.setKeystore(new org.thingsboard.server.common.transport.config.ssl.KeystoreSslCredentials()); + config.init(); + return config; + } + + private MqttSslHandlerProvider createMqttSslHandlerProvider(SslCredentialsConfig credentialsConfig) { + MqttSslHandlerProvider provider = new MqttSslHandlerProvider(); + ReflectionTestUtils.setField(provider, "sslProtocol", "TLSv1.2"); + ReflectionTestUtils.setField(provider, "mqttSslCredentialsConfig", credentialsConfig); + ReflectionTestUtils.setField(provider, "transportService", transportService); + provider.afterSingletonsInstantiated(); + return provider; + } + + /** + * Triggers SSLContext creation through the provider's getSslHandler() path, + * then extracts the cached SSLContext for direct server socket use. + */ + private SSLContext getProviderSslContext(MqttSslHandlerProvider provider) { + provider.getSslHandler(); + return (SSLContext) ReflectionTestUtils.getField(provider, "sslContext"); + } + + private KeyPair generateKeyPair() throws Exception { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + return kpg.generateKeyPair(); + } + + private X509Certificate generateSelfSignedCert(KeyPair kp, String subjectDn) throws Exception { + X500Name subject = new X500Name(subjectDn); + Date now = new Date(); + Date expiry = new Date(now.getTime() + TimeUnit.DAYS.toMillis(1)); + return new JcaX509CertificateConverter().getCertificate( + new JcaX509v3CertificateBuilder( + subject, BigInteger.valueOf(System.nanoTime()), now, expiry, + subject, kp.getPublic()) + .build(new JcaContentSignerBuilder("SHA256withRSA").build(kp.getPrivate()))); + } + + private void writeCertPem(Path path, X509Certificate cert) throws Exception { + try (PemWriter writer = new PemWriter(new OutputStreamWriter(Files.newOutputStream(path)))) { + writer.writeObject(new PemObject("CERTIFICATE", cert.getEncoded())); + } + } + + private void writeKeyPem(Path path, KeyPair keyPair) throws Exception { + try (PemWriter writer = new PemWriter(new OutputStreamWriter(Files.newOutputStream(path)))) { + writer.writeObject(new PemObject("PRIVATE KEY", keyPair.getPrivate().getEncoded())); + } + } + + private SSLServerSocket createServerSocket(SSLContext ctx) throws Exception { + return (SSLServerSocket) ctx.getServerSocketFactory().createServerSocket(0, 1, InetAddress.getLoopbackAddress()); + } + + private X509Certificate doHandshakeAndGetServerCert(SSLServerSocket serverSocket) throws Exception { + Thread acceptor = new Thread(() -> { + try (var conn = serverSocket.accept()) { + conn.getInputStream().read(); + } catch (Exception ignored) {} + }); + acceptor.setDaemon(true); + acceptor.start(); + + SSLContext clientCtx = SSLContext.getInstance("TLSv1.2"); + clientCtx.init(null, new TrustManager[]{new TrustAllManager()}, null); + + try (SSLSocket client = (SSLSocket) clientCtx.getSocketFactory() + .createSocket(InetAddress.getLoopbackAddress(), serverSocket.getLocalPort())) { + client.setSoTimeout(5000); + client.startHandshake(); + + Certificate[] peerCerts = client.getSession().getPeerCertificates(); + assertThat(peerCerts).isNotEmpty(); + return (X509Certificate) peerCerts[0]; + } finally { + acceptor.join(5000); + } + } + + private static class TrustAllManager implements X509TrustManager { + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) { + + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) { + + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + } + +} From cfc3935860b2eed06d9ccb0444b41ecc56c87ac5 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 26 Mar 2026 12:07:46 +0200 Subject: [PATCH 10/25] Set TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED default to 'true' --- application/src/main/resources/thingsboard.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index e4bd13cdb3..f297fadde0 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1400,8 +1400,8 @@ transport: # X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.) reload: # Enable/disable automatic SSL certificates reload - enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:false}" - # Check interval in seconds for certificates reload + enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" + # Check an interval in seconds for certificate reload check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}" # CoAP server parameters From 25d3394e9f0700abad5f0250fb5c1704a86c3829 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 26 Mar 2026 11:10:39 +0100 Subject: [PATCH 11/25] Fix race condition in notification deduplication check The alreadyProcessed() method used separate get() and put() calls on the local cache, allowing concurrent threads in the notification executor pool to both read null and bypass deduplication, creating duplicate notifications. Replace with a single compute() call that atomically checks and updates the cache entry, preventing the race between concurrent trigger processing. Also fix: discard external cache timestamps that are more than 1 hour in the future (clock skew protection), and avoid reading back from the SOFT ref local cache when writing to external cache (GC could null it out). --- ...faultNotificationDeduplicationService.java | 56 +++--- ...tNotificationDeduplicationServiceTest.java | 160 ++++++++++++++++++ 2 files changed, 192 insertions(+), 24 deletions(-) create mode 100644 common/queue/src/test/java/org/thingsboard/server/queue/notification/DefaultNotificationDeduplicationServiceTest.java diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/notification/DefaultNotificationDeduplicationService.java b/common/queue/src/main/java/org/thingsboard/server/queue/notification/DefaultNotificationDeduplicationService.java index bf884958eb..279a2ddc7f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/notification/DefaultNotificationDeduplicationService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/notification/DefaultNotificationDeduplicationService.java @@ -32,6 +32,7 @@ import org.thingsboard.server.queue.util.PropertyUtils; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType.SOFT; @@ -59,41 +60,48 @@ public class DefaultNotificationDeduplicationService implements NotificationDedu } private boolean alreadyProcessed(NotificationRuleTrigger trigger, String deduplicationKey, boolean onlyLocalCache) { - Long lastProcessedTs = localCache.get(deduplicationKey); - if (lastProcessedTs == null && !onlyLocalCache) { - Cache externalCache = getExternalCache(); - if (externalCache != null) { - lastProcessedTs = externalCache.get(deduplicationKey, Long.class); - } else { - log.warn("Sent notifications cache is not set up"); + long deduplicationDuration = getDeduplicationDuration(trigger); + final long now = System.currentTimeMillis(); + boolean[] result = {false}; + + localCache.compute(deduplicationKey, (key, lastProcessedTs) -> { + if (lastProcessedTs == null && !onlyLocalCache) { + Cache externalCache = getExternalCache(); + if (externalCache != null) { + lastProcessedTs = externalCache.get(key, Long.class); + if (lastProcessedTs != null && lastProcessedTs > now + TimeUnit.HOURS.toMillis(1)) { + log.warn("Discarding dedup entry from external cache for key '{}': timestamp is {} ms in the future", + key, lastProcessedTs - now); + lastProcessedTs = null; + } + } else { + log.warn("Sent notifications cache is not set up"); + } } - } - boolean alreadyProcessed = false; - long deduplicationDuration = getDeduplicationDuration(trigger); - if (lastProcessedTs != null) { - long passed = System.currentTimeMillis() - lastProcessedTs; - log.trace("Deduplicating trigger {} by key '{}'. Deduplication duration: {} ms, passed: {} ms", - trigger.getType(), deduplicationKey, deduplicationDuration, passed); - if (deduplicationDuration == 0 || passed <= deduplicationDuration) { - alreadyProcessed = true; + if (lastProcessedTs != null) { + long passed = now - lastProcessedTs; + log.trace("Deduplicating trigger {} by key '{}'. Deduplication duration: {} ms, passed: {} ms", + trigger.getType(), key, deduplicationDuration, passed); + if (deduplicationDuration == 0 || passed <= deduplicationDuration) { + result[0] = true; + return lastProcessedTs; + } } - } - if (!alreadyProcessed) { - lastProcessedTs = System.currentTimeMillis(); - } - localCache.put(deduplicationKey, lastProcessedTs); + return now; + }); + if (!onlyLocalCache) { - if (!alreadyProcessed || deduplicationDuration == 0) { + if (!result[0] || deduplicationDuration == 0) { // if lastProcessedTs is changed or if deduplicating infinitely (so that cache value not removed by ttl) Cache externalCache = getExternalCache(); if (externalCache != null) { - externalCache.put(deduplicationKey, lastProcessedTs); + externalCache.put(deduplicationKey, now); } } } - return alreadyProcessed; + return result[0]; } public static String getDeduplicationKey(NotificationRuleTrigger trigger, NotificationRule rule) { diff --git a/common/queue/src/test/java/org/thingsboard/server/queue/notification/DefaultNotificationDeduplicationServiceTest.java b/common/queue/src/test/java/org/thingsboard/server/queue/notification/DefaultNotificationDeduplicationServiceTest.java new file mode 100644 index 0000000000..826c1e1ef6 --- /dev/null +++ b/common/queue/src/test/java/org/thingsboard/server/queue/notification/DefaultNotificationDeduplicationServiceTest.java @@ -0,0 +1,160 @@ +/** + * 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.queue.notification; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger; +import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class DefaultNotificationDeduplicationServiceTest { + + private static final int TIMEOUT = 30; + + private DefaultNotificationDeduplicationService deduplicationService; + private CacheManager cacheManager; + + @BeforeEach + void setUp() { + deduplicationService = new DefaultNotificationDeduplicationService(); + deduplicationService.setDeduplicationDurations(""); + cacheManager = new ConcurrentMapCacheManager(CacheConstants.SENT_NOTIFICATIONS_CACHE); + ReflectionTestUtils.setField(deduplicationService, "cacheManager", cacheManager); + } + + @Test + void testFirstTriggerIsNotDeduplicated() { + NotificationRuleTrigger trigger = mockTrigger(TimeUnit.HOURS.toMillis(1)); + NotificationRule rule = mockRule(); + + assertThat(deduplicationService.alreadyProcessed(trigger, rule)).isFalse(); + } + + @Test + void testSecondTriggerIsDeduplicated() { + NotificationRuleTrigger trigger = mockTrigger(TimeUnit.HOURS.toMillis(1)); + NotificationRule rule = mockRule(); + + assertThat(deduplicationService.alreadyProcessed(trigger, rule)).isFalse(); + assertThat(deduplicationService.alreadyProcessed(trigger, rule)).isTrue(); + } + + @Test + void testTriggerPassesAfterDeduplicationWindowExpires() { + NotificationRuleTrigger trigger = mockTrigger(50); // 50ms dedup window + NotificationRule rule = mockRule(); + + assertThat(deduplicationService.alreadyProcessed(trigger, rule)).isFalse(); + + try { + Thread.sleep(200); // wait well past the 50ms window + } catch (InterruptedException ignored) {} + + assertThat(deduplicationService.alreadyProcessed(trigger, rule)).isFalse(); + } + + @Test + void testFutureTimestampFromExternalCacheIsDiscarded() { + NotificationRuleTrigger trigger = mockTrigger(TimeUnit.HOURS.toMillis(1)); + NotificationRule rule = mockRule(); + String dedupKey = DefaultNotificationDeduplicationService.getDeduplicationKey(trigger, rule); + + // Put a timestamp 2 hours in the future into external cache + Cache externalCache = cacheManager.getCache(CacheConstants.SENT_NOTIFICATIONS_CACHE); + externalCache.put(dedupKey, System.currentTimeMillis() + TimeUnit.HOURS.toMillis(2)); + + // Should NOT be deduplicated — future timestamp must be discarded + assertThat(deduplicationService.alreadyProcessed(trigger, rule)).isFalse(); + } + + @Test + void testValidTimestampFromExternalCacheIsDeduplicated() { + NotificationRuleTrigger trigger = mockTrigger(TimeUnit.HOURS.toMillis(1)); + NotificationRule rule = mockRule(); + String dedupKey = DefaultNotificationDeduplicationService.getDeduplicationKey(trigger, rule); + + // Put a recent timestamp into external cache + Cache externalCache = cacheManager.getCache(CacheConstants.SENT_NOTIFICATIONS_CACHE); + externalCache.put(dedupKey, System.currentTimeMillis()); + + // Should be deduplicated — valid external cache entry + assertThat(deduplicationService.alreadyProcessed(trigger, rule)).isTrue(); + } + + @Test + void testConcurrentTriggersProduceExactlyOneNonDeduplicated() throws Exception { + NotificationRuleTrigger trigger = mockTrigger(TimeUnit.HOURS.toMillis(1)); + NotificationRule rule = mockRule(); + + int threadCount = 10; + CyclicBarrier barrier = new CyclicBarrier(threadCount); + List results = new CopyOnWriteArrayList<>(); + + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + try { + for (int i = 0; i < threadCount; i++) { + executor.submit(() -> { + try { + barrier.await(TIMEOUT, TimeUnit.SECONDS); + } catch (Exception ignored) {} + results.add(deduplicationService.alreadyProcessed(trigger, rule)); + }); + } + executor.shutdown(); + assertThat(executor.awaitTermination(TIMEOUT, TimeUnit.SECONDS)).isTrue(); + + assertThat(results).hasSize(threadCount); + assertThat(results.stream().filter(r -> !r).count()) + .as("exactly one trigger should pass through deduplication") + .isEqualTo(1); + } finally { + executor.shutdownNow(); + } + } + + private NotificationRuleTrigger mockTrigger(long deduplicationDurationMs) { + NotificationRuleTrigger trigger = mock(NotificationRuleTrigger.class); + when(trigger.getType()).thenReturn(NotificationRuleTriggerType.RESOURCES_SHORTAGE); + when(trigger.getDeduplicationKey()).thenReturn("test:dedup:key"); + when(trigger.getDefaultDeduplicationDuration()).thenReturn(deduplicationDurationMs); + when(trigger.getDeduplicationStrategy()).thenReturn(NotificationRuleTrigger.DeduplicationStrategy.ONLY_MATCHING); + return trigger; + } + + private NotificationRule mockRule() { + NotificationRule rule = mock(NotificationRule.class); + when(rule.getDeduplicationKey()).thenReturn("rule:key"); + return rule; + } + +} From ee878ed6d0ae95e9aff45f403e07583eb46ac260 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 27 Mar 2026 12:32:58 +0200 Subject: [PATCH 12/25] Improve CertificateReloadManagerTest to use Awaitility --- .../service/CertificateReloadManagerTest.java | 143 +++++++----------- 1 file changed, 53 insertions(+), 90 deletions(-) diff --git a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java index ba3aa66c70..3156897210 100644 --- a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java +++ b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java @@ -28,7 +28,10 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; public class CertificateReloadManagerTest { @@ -53,24 +56,28 @@ public class CertificateReloadManagerTest { } } + private void writeFileAndAwaitMtimeChange(Path path, String content, long baselineMtime) throws IOException { + Files.writeString(path, content); + await().atMost(2, SECONDS) + .pollInterval(10, MILLISECONDS) + .until(() -> Files.getLastModifiedTime(path).toMillis() != baselineMtime); + } + + private long mtime(Path path) throws IOException { + return Files.getLastModifiedTime(path).toMillis(); + } + @Test public void givenCertificateFileChanged_whenCheckForChanges_thenShouldTriggerReload() throws Exception { - CountDownLatch reloadLatch = new CountDownLatch(1); AtomicInteger reloadCount = new AtomicInteger(0); - certificateReloadManager.registerWatcher("test-cert", certFile, () -> { - reloadCount.incrementAndGet(); - reloadLatch.countDown(); - }); + certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); - TimeUnit.MILLISECONDS.sleep(100); - Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nTEST_CERT_V2_MODIFIED\n-----END CERTIFICATE-----\n"); + long baseline = mtime(certFile); + writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nTEST_CERT_V2_MODIFIED\n-----END CERTIFICATE-----\n", baseline); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); - boolean reloadTriggered = reloadLatch.await(2, TimeUnit.SECONDS); - - assertThat(reloadTriggered).isTrue(); assertThat(reloadCount.get()).isEqualTo(1); } @@ -80,9 +87,7 @@ public class CertificateReloadManagerTest { certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); - TimeUnit.MILLISECONDS.sleep(100); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); - TimeUnit.MILLISECONDS.sleep(100); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); assertThat(reloadCount.get()).isEqualTo(0); @@ -94,8 +99,6 @@ public class CertificateReloadManagerTest { certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); - TimeUnit.MILLISECONDS.sleep(100); - ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); assertThat(reloadCount.get()).isEqualTo(0); @@ -107,15 +110,10 @@ public class CertificateReloadManagerTest { certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); - TimeUnit.MILLISECONDS.sleep(100); - Files.delete(certFile); - TimeUnit.MILLISECONDS.sleep(100); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); - TimeUnit.MILLISECONDS.sleep(100); - // File deletion changes checksum from real hash to "", so reload is triggered assertThat(reloadCount.get()).isEqualTo(1); } @@ -133,22 +131,19 @@ public class CertificateReloadManagerTest { Path keyFile = tempDir.resolve("test-key.pem"); Files.writeString(keyFile, "-----BEGIN PRIVATE KEY-----\nTEST_KEY_V1\n-----END PRIVATE KEY-----\n"); - CountDownLatch certReloadLatch = new CountDownLatch(1); - CountDownLatch keyReloadLatch = new CountDownLatch(1); + AtomicInteger certReloadCount = new AtomicInteger(0); + AtomicInteger keyReloadCount = new AtomicInteger(0); - certificateReloadManager.registerWatcher("test-cert", certFile, certReloadLatch::countDown); - certificateReloadManager.registerWatcher("test-key", keyFile, keyReloadLatch::countDown); + certificateReloadManager.registerWatcher("test-cert", certFile, certReloadCount::incrementAndGet); + certificateReloadManager.registerWatcher("test-key", keyFile, keyReloadCount::incrementAndGet); - TimeUnit.MILLISECONDS.sleep(100); - Files.writeString(keyFile, "-----BEGIN PRIVATE KEY-----\nTEST_KEY_V2_MODIFIED\n-----END PRIVATE KEY-----\n"); - TimeUnit.MILLISECONDS.sleep(100); + long baseline = mtime(keyFile); + writeFileAndAwaitMtimeChange(keyFile, "-----BEGIN PRIVATE KEY-----\nTEST_KEY_V2_MODIFIED\n-----END PRIVATE KEY-----\n", baseline); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); - boolean keyReloaded = keyReloadLatch.await(2, TimeUnit.SECONDS); - - assertThat(keyReloaded).isTrue(); - assertThat(certReloadLatch.getCount()).isEqualTo(1); + assertThat(keyReloadCount.get()).isEqualTo(1); + assertThat(certReloadCount.get()).isEqualTo(0); } @Test @@ -162,14 +157,13 @@ public class CertificateReloadManagerTest { certificateReloadManager.registerWatcher("test-cert1", certFile, reload1Count::incrementAndGet); certificateReloadManager.registerWatcher("test-cert2", cert2File, reload2Count::incrementAndGet); - TimeUnit.MILLISECONDS.sleep(100); - Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED1\n-----END CERTIFICATE-----\n"); - Files.writeString(cert2File, "-----BEGIN CERTIFICATE-----\nMODIFIED2\n-----END CERTIFICATE-----\n"); - TimeUnit.MILLISECONDS.sleep(100); + long baseline1 = mtime(certFile); + long baseline2 = mtime(cert2File); + writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED1\n-----END CERTIFICATE-----\n", baseline1); + writeFileAndAwaitMtimeChange(cert2File, "-----BEGIN CERTIFICATE-----\nMODIFIED2\n-----END CERTIFICATE-----\n", baseline2); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); - Thread.sleep(200); assertThat(reload1Count.get()).isEqualTo(1); assertThat(reload2Count.get()).isEqualTo(1); } @@ -186,14 +180,13 @@ public class CertificateReloadManagerTest { }); certificateReloadManager.registerWatcher("test-cert2", cert2File, reload2Count::incrementAndGet); - TimeUnit.MILLISECONDS.sleep(100); - Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED1\n-----END CERTIFICATE-----\n"); - Files.writeString(cert2File, "-----BEGIN CERTIFICATE-----\nMODIFIED2\n-----END CERTIFICATE-----\n"); - TimeUnit.MILLISECONDS.sleep(100); + long baseline1 = mtime(certFile); + long baseline2 = mtime(cert2File); + writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED1\n-----END CERTIFICATE-----\n", baseline1); + writeFileAndAwaitMtimeChange(cert2File, "-----BEGIN CERTIFICATE-----\nMODIFIED2\n-----END CERTIFICATE-----\n", baseline2); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); - Thread.sleep(200); assertThat(reload2Count.get()).isEqualTo(1); } @@ -203,44 +196,31 @@ public class CertificateReloadManagerTest { certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); - TimeUnit.MILLISECONDS.sleep(100); - Files.delete(certFile); - TimeUnit.MILLISECONDS.sleep(100); - ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); + assertThat(reloadCount.get()).isEqualTo(1); Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nNEW_CERT\n-----END CERTIFICATE-----\n"); - TimeUnit.MILLISECONDS.sleep(100); - ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); - - Thread.sleep(200); assertThat(reloadCount.get()).isEqualTo(2); } @Test public void givenRapidFileModifications_whenCheckForChanges_thenShouldDetectLatestChange() throws Exception { - CountDownLatch reloadLatch = new CountDownLatch(1); AtomicInteger reloadCount = new AtomicInteger(0); - certificateReloadManager.registerWatcher("test-cert", certFile, () -> { - reloadCount.incrementAndGet(); - reloadLatch.countDown(); - }); - - TimeUnit.MILLISECONDS.sleep(100); + certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); + long baseline = mtime(certFile); for (int i = 0; i < 5; i++) { Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nCERT_VERSION_" + i + "\n-----END CERTIFICATE-----\n"); } - TimeUnit.MILLISECONDS.sleep(100); + await().atMost(2, SECONDS) + .pollInterval(10, MILLISECONDS) + .until(() -> mtime(certFile) != baseline); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); - boolean reloadTriggered = reloadLatch.await(2, TimeUnit.SECONDS); - - assertThat(reloadTriggered).isTrue(); assertThat(reloadCount.get()).isEqualTo(1); } @@ -252,9 +232,8 @@ public class CertificateReloadManagerTest { certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); - TimeUnit.MILLISECONDS.sleep(100); - Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED\n-----END CERTIFICATE-----\n"); - TimeUnit.MILLISECONDS.sleep(100); + long baseline = mtime(certFile); + writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED\n-----END CERTIFICATE-----\n", baseline); for (int i = 0; i < 5; i++) { new Thread(() -> { @@ -273,7 +252,6 @@ public class CertificateReloadManagerTest { boolean completed = doneLatch.await(5, TimeUnit.SECONDS); assertThat(completed).isTrue(); - // With atomic checkAndReload, exactly one reload should happen assertThat(reloadCount.get()).isEqualTo(1); } @@ -284,14 +262,11 @@ public class CertificateReloadManagerTest { certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); - TimeUnit.MILLISECONDS.sleep(100); - - Files.writeString(certFile, originalContent); - TimeUnit.MILLISECONDS.sleep(100); + long baseline = mtime(certFile); + writeFileAndAwaitMtimeChange(certFile, originalContent, baseline); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); - Thread.sleep(200); assertThat(reloadCount.get()).isEqualTo(0); } @@ -304,11 +279,9 @@ public class CertificateReloadManagerTest { throw new RuntimeException("Persistent failure"); }); - TimeUnit.MILLISECONDS.sleep(100); - Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n"); - TimeUnit.MILLISECONDS.sleep(100); + long baseline = mtime(certFile); + writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n", baseline); - // Retry up to MAX_CONSECUTIVE_FAILURES (10) + a few extra to confirm it stops for (int i = 0; i < 15; i++) { ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); } @@ -328,21 +301,16 @@ public class CertificateReloadManagerTest { } }); - TimeUnit.MILLISECONDS.sleep(100); - Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n"); - TimeUnit.MILLISECONDS.sleep(100); + long baseline = mtime(certFile); + writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n", baseline); - // First attempt fails ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); assertThat(reloadAttempts.get()).isEqualTo(1); - // Fix the callback and change the file to new content shouldFail.set(0); - TimeUnit.MILLISECONDS.sleep(100); - Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nGOOD_CERT\n-----END CERTIFICATE-----\n"); - TimeUnit.MILLISECONDS.sleep(100); + long baseline2 = mtime(certFile); + writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nGOOD_CERT\n-----END CERTIFICATE-----\n", baseline2); - // Should reset failure counter and succeed because file content changed ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); assertThat(reloadAttempts.get()).isEqualTo(2); } @@ -359,23 +327,18 @@ public class CertificateReloadManagerTest { } }); - TimeUnit.MILLISECONDS.sleep(100); - Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n"); - TimeUnit.MILLISECONDS.sleep(100); + long baseline = mtime(certFile); + writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n", baseline); - // Exhaust all retries for (int i = 0; i < 15; i++) { ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); } assertThat(reloadAttempts.get()).isEqualTo(10); - // Fix callback and change file to new content shouldFail.set(0); - TimeUnit.MILLISECONDS.sleep(100); - Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nFIXED_CERT\n-----END CERTIFICATE-----\n"); - TimeUnit.MILLISECONDS.sleep(100); + long baseline2 = mtime(certFile); + writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nFIXED_CERT\n-----END CERTIFICATE-----\n", baseline2); - // Should detect new content, reset counter, and succeed ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); assertThat(reloadAttempts.get()).isEqualTo(11); } From 9ccf94f93b9cdc408f85aa60ee28abd0ec687415 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 9 Apr 2026 12:19:06 +0300 Subject: [PATCH 13/25] Debounce LwM2M server reload to avoid double restart when both credentials change --- .../coapserver/DefaultCoapServerService.java | 4 +- .../config/LwM2MTransportServerConfig.java | 42 ++++++- ...wM2MTransportServerConfigDebounceTest.java | 106 ++++++++++++++++++ .../src/main/resources/tb-coap-transport.yml | 2 +- .../src/main/resources/tb-http-transport.yml | 2 +- .../src/main/resources/tb-lwm2m-transport.yml | 2 +- .../src/main/resources/tb-mqtt-transport.yml | 2 +- 7 files changed, 148 insertions(+), 12 deletions(-) create mode 100644 common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfigDebounceTest.java diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java index 03de8b7b91..3b7248ee72 100644 --- a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java @@ -196,8 +196,8 @@ public class DefaultCoapServerService implements CoapServerService, SmartInitial DTLSConnector newConnector = createDtlsConnector(dtlsConnectorConfig); CoapEndpoint newEndpoint = buildDtlsEndpoint(networkConfig, newConnector); - // Californium binds the DTLS port at connector construction time, so we must stop the old - // endpoint first to release the port. This creates a brief window where the port is unbound; + // We must stop the old endpoint before starting the new one so they don't compete for the same DTLS port. + // This creates a brief window where the port is unbound; // if the new endpoint fails to start, we attempt to restore the old one (see rollback below). if (oldDtlsEndpoint != null) { log.info("Stopping old DTLS endpoint to release the port..."); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java index 6034219c4d..c092dcea77 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.lwm2m.config; import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -27,12 +28,17 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.TbProperty; import org.thingsboard.server.common.transport.config.ssl.SslCredentials; import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; @Slf4j @Component @@ -40,6 +46,13 @@ import java.util.concurrent.CopyOnWriteArrayList; @ConfigurationProperties(prefix = "transport.lwm2m") public class LwM2MTransportServerConfig implements LwM2MSecureServerConfig { + private static final long RELOAD_DEBOUNCE_SECONDS = 2; + + private final List serverReloadCallbacks = new CopyOnWriteArrayList<>(); + private final ScheduledExecutorService reloadDebouncer = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("lwm2m-reload-debouncer")); + + private volatile ScheduledFuture pendingReload; + @Getter @Value("${transport.lwm2m.dtls.retransmission_timeout:9000}") private int dtlsRetransmissionTimeout; @@ -136,25 +149,42 @@ public class LwM2MTransportServerConfig implements LwM2MSecureServerConfig { @Qualifier("lwm2mTrustCredentials") private SslCredentialsConfig trustCredentialsConfig; - private final List serverReloadCallbacks = new CopyOnWriteArrayList<>(); - @PostConstruct public void init() { credentialsConfig.registerReloadCallback(() -> { - log.info("LwM2M Server DTLS certificates reloaded. Triggering server reload..."); - notifyServerReload(); + log.info("LwM2M Server DTLS certificates reloaded. Scheduling debounced server reload..."); + scheduleServerReload(); }); trustCredentialsConfig.registerReloadCallback(() -> { - log.info("LwM2M Trust certificates reloaded. Triggering server reload..."); - notifyServerReload(); + log.info("LwM2M Trust certificates reloaded. Scheduling debounced server reload..."); + scheduleServerReload(); }); } + @PreDestroy + public void destroy() { + reloadDebouncer.shutdownNow(); + } + public void registerServerReloadCallback(Runnable callback) { serverReloadCallbacks.add(callback); } + /** + * Debounces server reload so that if both server and trust credentials change in the same + * poll cycle, only the 'single server recreation' is triggered after both are reloaded. + */ + private synchronized void scheduleServerReload() { + if (pendingReload != null) { + pendingReload.cancel(false); + } + pendingReload = reloadDebouncer.schedule(() -> { + log.info("Debounce window elapsed. Triggering LwM2M server reload..."); + notifyServerReload(); + }, RELOAD_DEBOUNCE_SECONDS, TimeUnit.SECONDS); + } + private void notifyServerReload() { for (Runnable callback : serverReloadCallbacks) { try { diff --git a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfigDebounceTest.java b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfigDebounceTest.java new file mode 100644 index 0000000000..1226d0266e --- /dev/null +++ b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfigDebounceTest.java @@ -0,0 +1,106 @@ +/** + * 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.transport.lwm2m.config; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig; + +import java.util.concurrent.atomic.AtomicInteger; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@ExtendWith(MockitoExtension.class) +public class LwM2MTransportServerConfigDebounceTest { + + private static final long DEBOUNCE_SECONDS = 2; // matches LwM2MTransportServerConfig.RELOAD_DEBOUNCE_SECONDS + + @Mock + private SslCredentialsConfig credentialsConfig; + + @Mock + private SslCredentialsConfig trustCredentialsConfig; + + private LwM2MTransportServerConfig config; + + @BeforeEach + public void setup() { + config = new LwM2MTransportServerConfig(); + ReflectionTestUtils.setField(config, "credentialsConfig", credentialsConfig); + ReflectionTestUtils.setField(config, "trustCredentialsConfig", trustCredentialsConfig); + } + + @AfterEach + public void teardown() { + config.destroy(); + } + + @Test + public void givenSingleTrigger_whenScheduleServerReload_thenCallbackFiresOnce() { + AtomicInteger callCount = new AtomicInteger(0); + config.registerServerReloadCallback(callCount::incrementAndGet); + + invokeScheduleServerReload(); + + await().atMost(DEBOUNCE_SECONDS + 2, SECONDS) + .untilAsserted(() -> assertThat(callCount.get()).isEqualTo(1)); + } + + @Test + public void givenTwoRapidTriggers_whenScheduleServerReload_thenCallbackFiresOnce() { + AtomicInteger callCount = new AtomicInteger(0); + config.registerServerReloadCallback(callCount::incrementAndGet); + + invokeScheduleServerReload(); + invokeScheduleServerReload(); + + await().atMost(DEBOUNCE_SECONDS + 2, SECONDS) + .untilAsserted(() -> assertThat(callCount.get()).isEqualTo(1)); + + // Wait extra to confirm no second invocation + await().during(DEBOUNCE_SECONDS + 1, SECONDS) + .atMost(DEBOUNCE_SECONDS + 2, SECONDS) + .untilAsserted(() -> assertThat(callCount.get()).isEqualTo(1)); + } + + @Test + public void givenTriggersOutsideDebounceWindow_whenScheduleServerReload_thenCallbackFiresTwice() { + AtomicInteger callCount = new AtomicInteger(0); + config.registerServerReloadCallback(callCount::incrementAndGet); + + invokeScheduleServerReload(); + + await().atMost(DEBOUNCE_SECONDS + 2, SECONDS) + .untilAsserted(() -> assertThat(callCount.get()).isEqualTo(1)); + + invokeScheduleServerReload(); + + await().atMost(DEBOUNCE_SECONDS + 2, SECONDS) + .untilAsserted(() -> assertThat(callCount.get()).isEqualTo(2)); + } + + private void invokeScheduleServerReload() { + ReflectionTestUtils.invokeMethod(config, "scheduleServerReload"); + } + +} diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index f9827d488f..3c1ef94f09 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -176,7 +176,7 @@ transport: # X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.) reload: # Enable/disable automatic SSL certificates reload - enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:false}" + enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" # Check interval in seconds for certificates reload check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}" diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index 7efa428a90..1b221d1fd9 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -207,7 +207,7 @@ transport: # X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.) reload: # Enable/disable automatic SSL certificates reload - enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:false}" + enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" # Check interval in seconds for certificates reload check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}" diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 543b11859b..6140122062 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -307,7 +307,7 @@ transport: # X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.) reload: # Enable/disable automatic SSL certificates reload - enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:false}" + enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" # Check interval in seconds for certificates reload check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}" diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index 60d3da8f8e..ac02fa396b 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -240,7 +240,7 @@ transport: # X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.) reload: # Enable/disable automatic SSL certificates reload - enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:false}" + enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" # Check interval in seconds for certificates reload check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}" From 63654ccb3bf03a9298a797ef29db18d01ea3ed12 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 9 Apr 2026 12:39:24 +0300 Subject: [PATCH 14/25] Fix typo --- application/src/main/resources/thingsboard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index f297fadde0..3bdfa31508 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1401,7 +1401,7 @@ transport: reload: # Enable/disable automatic SSL certificates reload enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" - # Check an interval in seconds for certificate reload + # Interval in seconds for certificate reload check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}" # CoAP server parameters From 1259fdbced1f002669b2394430667a1aa1d261bb Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 20 Apr 2026 16:12:12 +0300 Subject: [PATCH 15/25] Fixes after review --- .../server/DefaultLwM2mTransportService.java | 40 ++++++++++++++----- ...wM2MTransportServerConfigDebounceTest.java | 2 +- .../mqtt/MqttSslHandlerProvider.java | 12 ++++-- .../mqtt/MqttSslHandlerProviderTest.java | 33 +++++++-------- .../config/ssl/KeystoreSslCredentials.java | 8 ++-- .../config/ssl/PemSslCredentials.java | 8 ++-- .../config/ssl/SslCredentialsConfig.java | 23 ++++++----- .../service/CertificateReloadManager.java | 37 ++++++++++------- .../config/ssl/SslCredentialsConfigTest.java | 7 +++- 9 files changed, 101 insertions(+), 69 deletions(-) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java index 5815a47555..b72cf62d99 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java @@ -32,6 +32,7 @@ import org.eclipse.leshan.server.californium.LwM2mPskStore; import org.eclipse.leshan.server.californium.endpoint.CaliforniumServerEndpointsProvider; import org.eclipse.leshan.server.californium.endpoint.coap.CoapServerProtocolProvider; import org.eclipse.leshan.server.californium.endpoint.coaps.CoapsServerProtocolProvider; +import org.eclipse.leshan.server.endpoint.LwM2mServerEndpointsProvider; import org.eclipse.leshan.server.registration.RegistrationStore; import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.context.annotation.DependsOn; @@ -236,36 +237,41 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService, Smar log.info("Creating new LwM2M server with updated certificates..."); LeshanServer newServer = getLhServer(); - // Stop (not destroy) old server to release ports but keep it restartable for rollback + // Only cycle the endpoint providers (CoAP/DTLS). The RegistrationStore and SecurityStore are + // Spring singletons shared with newServer — calling oldServer.stop()/destroy() would propagate + // to them (LeshanServer.stop/destroy propagate to Stoppable/Destroyable stores), which would + // shut down the shared schedulers (TbInMemoryRegistrationStore.destroy calls schedExecutor.shutdownNow), + // killing newServer's cleaner tasks. Leaving the stores running preserves existing device + // registrations across the swap — clients only need to re-establish DTLS on next uplink. if (oldServer != null) { - log.info("Stopping old LwM2M server to release ports..."); + log.info("Stopping old LwM2M endpoints to release ports..."); if (oldListener != null) { oldServer.getRegistrationService().removeListener(oldListener.registrationListener); oldServer.getPresenceService().removeListener(oldListener.presenceListener); oldServer.getObservationService().removeListener(oldListener.observationListener); oldServer.getSendService().removeListener(oldListener.sendListener); } - oldServer.stop(); + stopEndpoints(oldServer); } try { newServer.start(); } catch (Exception e) { log.error("Failed to start new LwM2M server", e); - newServer.destroy(); - // Attempt to restart the old server (only stopped, not destroyed) + destroyEndpoints(newServer); + // Attempt to restart the old endpoints (shared stores are still running). if (oldServer != null) { try { - oldServer.start(); + startEndpoints(oldServer); if (oldListener != null) { oldServer.getRegistrationService().addListener(oldListener.registrationListener); oldServer.getPresenceService().addListener(oldListener.presenceListener); oldServer.getObservationService().addListener(oldListener.observationListener); oldServer.getSendService().addListener(oldListener.sendListener); } - log.info("Restored old LwM2M server successfully."); + log.info("Restored old LwM2M endpoints successfully."); } catch (Exception restoreEx) { - log.error("Failed to restore old LwM2M server", restoreEx); + log.error("Failed to restore old LwM2M endpoints", restoreEx); } } throw e; @@ -280,14 +286,26 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService, Smar this.server = newServer; this.context.setServer(newServer); this.serverListener = newListener; - log.info("New LwM2M server started successfully."); + log.info("New LwM2M server started with refreshed certificates. Existing device registrations preserved; clients will re-establish DTLS on next uplink."); - // Destroy old server only after successful swap + // Destroy old endpoints only — leave the shared stores alone. if (oldServer != null) { - oldServer.destroy(); + destroyEndpoints(oldServer); } } + private void stopEndpoints(LeshanServer server) { + server.getEndpointsProvider().forEach(LwM2mServerEndpointsProvider::stop); + } + + private void startEndpoints(LeshanServer server) { + server.getEndpointsProvider().forEach(LwM2mServerEndpointsProvider::start); + } + + private void destroyEndpoints(LeshanServer server) { + server.getEndpointsProvider().forEach(LwM2mServerEndpointsProvider::destroy); + } + @Override public String getName() { return DataConstants.LWM2M_TRANSPORT_NAME; diff --git a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfigDebounceTest.java b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfigDebounceTest.java index 1226d0266e..93f305a190 100644 --- a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfigDebounceTest.java +++ b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfigDebounceTest.java @@ -33,7 +33,7 @@ import static org.awaitility.Awaitility.await; @ExtendWith(MockitoExtension.class) public class LwM2MTransportServerConfigDebounceTest { - private static final long DEBOUNCE_SECONDS = 2; // matches LwM2MTransportServerConfig.RELOAD_DEBOUNCE_SECONDS + private static final long DEBOUNCE_SECONDS = (long) ReflectionTestUtils.getField(LwM2MTransportServerConfig.class, "RELOAD_DEBOUNCE_SECONDS"); @Mock private SslCredentialsConfig credentialsConfig; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java index fcb58bc20c..a954d7ae7f 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java @@ -71,15 +71,21 @@ public class MqttSslHandlerProvider implements SmartInitializingSingleton { @Override public void afterSingletonsInstantiated() { + // Eagerly build the initial context so the handshake path is a lock-free volatile read. + this.sslContext = createSslContext(); mqttSslCredentialsConfig.registerReloadCallback(() -> { - log.info("MQTT SSL certificates reloaded. Invalidating SSL context..."); - sslContext = null; - log.info("MQTT SSL context invalidated. Will be recreated on next connection."); + log.info("MQTT SSL certificates reloaded. Rebuilding SSL context..."); + // Build the new context first; if it fails, the old one stays in place, and + // the exception propagates to CertificateReloadManager's retry/backoff logic. + this.sslContext = createSslContext(); + log.info("MQTT SSL context rebuilt. New connections will use the new certificate."); }); } public SslHandler getSslHandler() { SSLContext ctx = sslContext; + // Defensive lazy init in case afterSingletonsInstantiated hasn't run yet (e.g., test wiring). + // In normal operation ctx is non-null here, so the handshake path is lock-free. if (ctx == null) { synchronized (this) { ctx = sslContext; diff --git a/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProviderTest.java b/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProviderTest.java index 96183c2934..8c4f3c7a29 100644 --- a/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProviderTest.java +++ b/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProviderTest.java @@ -89,38 +89,33 @@ public class MqttSslHandlerProviderTest { } @Test - public void givenCertificatesReloaded_whenGetSslHandler_thenShouldRecreateSSLContext() { + public void givenCertificatesReloaded_whenReloadCallbackInvoked_thenShouldRebuildSSLContextEagerly() { sslHandlerProvider.afterSingletonsInstantiated(); ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); Runnable reloadCallback = callbackCaptor.getValue(); - SslHandler handler1 = sslHandlerProvider.getSslHandler(); SSLContext initialContext = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); assertThat(initialContext).isNotNull(); reloadCallback.run(); - assertThat(handler1).isNotNull(); + // After reload the context is rebuilt eagerly (no null-invalidation), so handshakes stay lock-free. SSLContext contextAfterReload = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); - assertThat(contextAfterReload).isNull(); + assertThat(contextAfterReload).isNotNull(); + assertThat(contextAfterReload).isNotSameAs(initialContext); - SslHandler handler2 = sslHandlerProvider.getSslHandler(); - SSLContext newContext = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); - - assertThat(handler2).isNotNull(); - assertThat(newContext).isNotNull(); - assertThat(newContext).isNotSameAs(initialContext); + SslHandler handler = sslHandlerProvider.getSslHandler(); + assertThat(handler).isNotNull(); } @Test - public void givenConcurrentGetSslHandlerCalls_whenSSLContextNull_thenShouldCreateOnlyOnce() throws Exception { + public void givenConcurrentGetSslHandlerCalls_whenContextAlreadyBuilt_thenAllReadsReturnSameContext() throws Exception { sslHandlerProvider.afterSingletonsInstantiated(); - ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(Runnable.class); - verify(mockCredentialsConfig).registerReloadCallback(callbackCaptor.capture()); - callbackCaptor.getValue().run(); + SSLContext contextBefore = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); + assertThat(contextBefore).isNotNull(); CountDownLatch startLatch = new CountDownLatch(1); CountDownLatch doneLatch = new CountDownLatch(5); @@ -143,12 +138,13 @@ public class MqttSslHandlerProviderTest { boolean completed = doneLatch.await(5, TimeUnit.SECONDS); assertThat(completed).isTrue(); - SSLContext context = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); - assertThat(context).isNotNull(); + // Concurrent handshakes read the same pre-built context without the old sync bottleneck. + SSLContext contextAfter = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); + assertThat(contextAfter).isSameAs(contextBefore); } @Test - public void givenReloadCallback_whenInvoked_thenShouldInvalidateSSLContext() { + public void givenReloadCallback_whenInvoked_thenShouldSwapSSLContextEagerly() { sslHandlerProvider.afterSingletonsInstantiated(); sslHandlerProvider.getSslHandler(); @@ -161,7 +157,8 @@ public class MqttSslHandlerProviderTest { callbackCaptor.getValue().run(); SSLContext contextAfterReload = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); - assertThat(contextAfterReload).isNull(); + assertThat(contextAfterReload).isNotNull(); + assertThat(contextAfterReload).isNotSameAs(initialContext); } @Test diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/KeystoreSslCredentials.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/KeystoreSslCredentials.java index 7cbc4403b7..3986704a33 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/KeystoreSslCredentials.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/KeystoreSslCredentials.java @@ -22,7 +22,6 @@ import org.thingsboard.server.common.data.StringUtils; import java.io.IOException; import java.io.InputStream; -import java.nio.file.Files; import java.nio.file.Path; import java.security.GeneralSecurityException; import java.security.KeyStore; @@ -62,10 +61,9 @@ public class KeystoreSslCredentials extends AbstractSslCredentials { @Override public List getCertificateFilePaths() { if (!StringUtils.isEmpty(storeFile) && !storeFile.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) { - Path resolved = Path.of(storeFile).toAbsolutePath(); - if (Files.exists(resolved)) { - return Collections.singletonList(resolved); - } + // Include the path even if the file doesn't exist yet — the watcher uses mtime=0 / checksum="" as + // baseline, so a late-appearing file (e.g., mounted after boot) will be detected and trigger a reload. + return Collections.singletonList(Path.of(storeFile).toAbsolutePath()); } return Collections.emptyList(); } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/PemSslCredentials.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/PemSslCredentials.java index c6eb75698e..fb2e5a4a0d 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/PemSslCredentials.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/PemSslCredentials.java @@ -33,7 +33,6 @@ import org.thingsboard.server.common.data.StringUtils; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.nio.file.Files; import java.nio.file.Path; import java.security.GeneralSecurityException; import java.security.KeyStore; @@ -152,10 +151,9 @@ public class PemSslCredentials extends AbstractSslCredentials { private static void addIfFileSystemPath(List paths, String filePath) { if (!StringUtils.isEmpty(filePath) && !filePath.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) { - Path resolved = Path.of(filePath).toAbsolutePath(); - if (Files.exists(resolved)) { - paths.add(resolved); - } + // Include the path even if the file doesn't exist yet — the watcher uses mtime=0 / checksum="" as + // baseline, so a late-appearing file (e.g. mounted after boot) will be detected and trigger a reload. + paths.add(Path.of(filePath).toAbsolutePath()); } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsConfig.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsConfig.java index 3646c4f37d..e747de4931 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsConfig.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsConfig.java @@ -68,20 +68,23 @@ public class SslCredentialsConfig { } public void onCertificateFileChanged() { + log.info("{}: Certificate file changed. Reloading SSL credentials...", name); try { - log.info("{}: Certificate file changed. Reloading SSL credentials...", name); this.credentials.reload(this.trustsOnly); - log.info("{}: SSL credentials reloaded successfully.", name); - - for (Runnable callback : reloadCallbacks) { - try { - callback.run(); - } catch (Exception e) { - log.error("{}: Error executing reload callback", name, e); - } - } } catch (Exception e) { log.error("{}: Failed to reload SSL credentials", name, e); + // Rethrow, so CertificateReloadManager's watcher counts this as a failure + // and applies MAX_CONSECUTIVE_FAILURES backoff instead of treating it as a successful reload. + throw new RuntimeException(name + ": Failed to reload SSL credentials", e); + } + log.info("{}: SSL credentials reloaded successfully.", name); + + for (Runnable callback : reloadCallbacks) { + try { + callback.run(); + } catch (Exception e) { + log.error("{}: Error executing reload callback", name, e); + } } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java index eec84a22f0..63f2247aba 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java @@ -32,6 +32,7 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.security.MessageDigest; +import java.util.ArrayList; import java.util.Base64; import java.util.HashMap; import java.util.List; @@ -48,7 +49,7 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis private static final int MAX_CONSECUTIVE_FAILURES = 10; - @Value("${transport.ssl.certificate.reload.enabled:false}") + @Value("${transport.ssl.certificate.reload.enabled:true}") private boolean reloadEnabled; @Value("${transport.ssl.certificate.reload.check_interval_seconds:60}") @@ -107,19 +108,24 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis continue; } - List existingPaths = filePaths.stream() - .filter(p -> p != null && Files.exists(p)) - .toList(); - + // Register all configured paths, including those that don't exist yet — the watcher uses + // mtime=0 / checksum="" as baseline, so files that appear later (e.g. delayed mounts) are + // picked up and trigger a reload on the next poll. + List pathsToWatch = new ArrayList<>(filePaths.size()); for (Path filePath : filePaths) { - if (filePath == null || !Files.exists(filePath)) { - log.warn("Certificate file does not exist: {} (from {})", filePath, config.getName()); + if (filePath == null) { + continue; + } + pathsToWatch.add(filePath); + if (!Files.exists(filePath)) { + log.warn("Certificate file does not exist yet: {} (from {}) — will be watched and picked up when it appears", + filePath, config.getName()); } } - if (!existingPaths.isEmpty()) { - registerWatcher(config.getName(), existingPaths, config::onCertificateFileChanged); - log.info("Registered certificate watcher: {} -> {}", config.getName(), existingPaths); + if (!pathsToWatch.isEmpty()) { + registerWatcher(config.getName(), pathsToWatch, config::onCertificateFileChanged); + log.info("Registered certificate watcher: {} -> {}", config.getName(), pathsToWatch); } } catch (Exception e) { @@ -190,10 +196,13 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis return; } - // Compute combined checksum of all files + // Capture mtimes and checksums together before the callback runs. + // Pairing a post-callback mtime with a pre-callback checksum would let a write-during-reload be missed on the next poll. + Map currentModifiedTimes = new HashMap<>(); Map currentChecksums = new HashMap<>(); StringBuilder combined = new StringBuilder(); for (Path path : paths) { + currentModifiedTimes.put(path, getLastModifiedTime(path)); String checksum = calculateChecksum(path); currentChecksums.put(path, checksum); if (!combined.isEmpty()) { @@ -216,7 +225,7 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis if (combinedChecksum.equals(oldCombinedChecksum)) { // Content unchanged, just update modification times for (Path path : paths) { - lastModifiedMap.put(path, getLastModifiedTime(path)); + lastModifiedMap.put(path, currentModifiedTimes.get(path)); } return; } @@ -230,7 +239,7 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) { // Update modification times to avoid re-checking mtime and re-computing checksums every poll cycle for (Path path : paths) { - lastModifiedMap.put(path, getLastModifiedTime(path)); + lastModifiedMap.put(path, currentModifiedTimes.get(path)); } return; } @@ -239,7 +248,7 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis log.info("Certificate change detected for: {}. Triggering reload...", name); reloadCallback.run(); for (Path path : paths) { - lastModifiedMap.put(path, getLastModifiedTime(path)); + lastModifiedMap.put(path, currentModifiedTimes.get(path)); lastChecksumMap.put(path, currentChecksums.get(path)); } consecutiveFailures = 0; diff --git a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsConfigTest.java b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsConfigTest.java index 6e16b2dc83..ec6d2a0117 100644 --- a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsConfigTest.java +++ b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsConfigTest.java @@ -26,6 +26,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.verify; @@ -114,7 +115,7 @@ public class SslCredentialsConfigTest { } @Test - public void givenCredentialsReloadFails_whenCertificateChanged_thenCallbacksShouldNotBeCalled() throws Exception { + public void givenCredentialsReloadFails_whenCertificateChanged_thenShouldRethrowAndNotCallCallbacks() throws Exception { AtomicInteger callbackCount = new AtomicInteger(0); config.registerReloadCallback(callbackCount::incrementAndGet); @@ -122,7 +123,9 @@ public class SslCredentialsConfigTest { doThrow(new RuntimeException("Simulated reload failure")).when(mockCredentials).reload(false); - config.onCertificateFileChanged(); + assertThatThrownBy(() -> config.onCertificateFileChanged()) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("Failed to reload SSL credentials"); assertThat(callbackCount.get()).isEqualTo(0); } From c63665ab81c88176bb0908cf8d9b1b1b4e2aa643 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 21 Apr 2026 10:56:37 +0300 Subject: [PATCH 16/25] Fixed CVE-2026-39364, CVE-2026-39363 --- ui-ngx/package.json | 32 +- ....22.patch => @angular+build+20.3.24.patch} | 0 ...3.18.patch => @angular+core+20.3.19.patch} | 2 +- ui-ngx/yarn.lock | 526 +++++++++--------- 4 files changed, 280 insertions(+), 280 deletions(-) rename ui-ngx/patches/{@angular+build+20.3.22.patch => @angular+build+20.3.24.patch} (100%) rename ui-ngx/patches/{@angular+core+20.3.18.patch => @angular+core+20.3.19.patch} (97%) diff --git a/ui-ngx/package.json b/ui-ngx/package.json index b88a0583e2..4f34216e6c 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -13,16 +13,16 @@ }, "private": true, "dependencies": { - "@angular/animations": "20.3.18", + "@angular/animations": "20.3.19", "@angular/cdk": "20.2.14", - "@angular/common": "20.3.18", - "@angular/compiler": "20.3.18", - "@angular/core": "20.3.18", - "@angular/forms": "20.3.18", + "@angular/common": "20.3.19", + "@angular/compiler": "20.3.19", + "@angular/core": "20.3.19", + "@angular/forms": "20.3.19", "@angular/material": "20.2.14", - "@angular/platform-browser": "20.3.18", - "@angular/platform-browser-dynamic": "20.3.18", - "@angular/router": "20.3.18", + "@angular/platform-browser": "20.3.19", + "@angular/platform-browser-dynamic": "20.3.19", + "@angular/router": "20.3.19", "@auth0/angular-jwt": "^5.2.0", "@flowjs/flow.js": "^2.14.1", "@flowjs/ngx-flow": "20.0.2", @@ -94,13 +94,13 @@ }, "devDependencies": { "@angular-builders/custom-esbuild": "20.0.0", - "@angular-devkit/build-angular": "20.3.22", - "@angular-devkit/core": "20.3.22", - "@angular-devkit/schematics": "20.3.22", - "@angular/build": "20.3.22", - "@angular/cli": "20.3.22", - "@angular/compiler-cli": "20.3.18", - "@angular/language-service": "20.3.18", + "@angular-devkit/build-angular": "20.3.24", + "@angular-devkit/core": "20.3.24", + "@angular-devkit/schematics": "20.3.24", + "@angular/build": "20.3.24", + "@angular/cli": "20.3.24", + "@angular/compiler-cli": "20.3.19", + "@angular/language-service": "20.3.19", "@types/ace-diff": "^2.1.4", "@types/canvas-gauges": "^2.1.8", "@types/flot": "^0.0.36", @@ -139,7 +139,7 @@ "ace-builds": "1.43.6", "tinymce": "6.8.6", "@babel/core": "7.28.3", - "esbuild": "0.25.9", + "esbuild": "0.28.0", "rollup": "4.59.0", "jquery.terminal/**/form-data": ">=4.0.4", "js-beautify/**/minimatch": "^9.0.7" diff --git a/ui-ngx/patches/@angular+build+20.3.22.patch b/ui-ngx/patches/@angular+build+20.3.24.patch similarity index 100% rename from ui-ngx/patches/@angular+build+20.3.22.patch rename to ui-ngx/patches/@angular+build+20.3.24.patch diff --git a/ui-ngx/patches/@angular+core+20.3.18.patch b/ui-ngx/patches/@angular+core+20.3.19.patch similarity index 97% rename from ui-ngx/patches/@angular+core+20.3.18.patch rename to ui-ngx/patches/@angular+core+20.3.19.patch index 12ceb3739d..5295946f7b 100644 --- a/ui-ngx/patches/@angular+core+20.3.18.patch +++ b/ui-ngx/patches/@angular+core+20.3.19.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@angular/core/fesm2022/debug_node.mjs b/node_modules/@angular/core/fesm2022/debug_node.mjs -index 35c61af..d89462b 100755 +index 4f7d936..4a98b2c 100755 --- a/node_modules/@angular/core/fesm2022/debug_node.mjs +++ b/node_modules/@angular/core/fesm2022/debug_node.mjs @@ -9428,13 +9428,13 @@ function findDirectiveDefMatches(tView, tNode) { diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 7a77f42959..ec9183bcc7 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -160,24 +160,24 @@ "@angular-devkit/core" "^20.0.0" "@angular/build" "^20.0.0" -"@angular-devkit/architect@0.2003.22", "@angular-devkit/architect@>= 0.2000.0 < 0.2100.0", "@angular-devkit/architect@>=0.2000.0 < 0.2100.0": - version "0.2003.22" - resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.2003.22.tgz#cb660579890be1d0622339bc74a85b6f1697ae5f" - integrity sha512-gxVOslVweD+Co6gpRVlByHus/3HVAnsl99MobS9PBh8vh2g6bJ011PBgl0TKsP/pqBGawZOkJXYrRPeMKnobYA== +"@angular-devkit/architect@0.2003.24", "@angular-devkit/architect@>= 0.2000.0 < 0.2100.0", "@angular-devkit/architect@>=0.2000.0 < 0.2100.0": + version "0.2003.24" + resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.2003.24.tgz#1c59822ffeada4248f669424c7c0534f39fe8bc5" + integrity sha512-E7mCdkL6SWnW60G1nGLuugmsopza/eVIrDWB1y0vLkWN8gepOvnHz2Uf637kdzed/F1WoqR+dhv1SfsaJapzKA== dependencies: - "@angular-devkit/core" "20.3.22" + "@angular-devkit/core" "20.3.24" rxjs "7.8.2" -"@angular-devkit/build-angular@20.3.22": - version "20.3.22" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-20.3.22.tgz#a96ba0d41a708ed3402f2568db343b1e85fe0e4e" - integrity sha512-PnKIRue/j30sWLWC9q2T3WwE5+GJekDX/zCXTYgjW6u0y/kt+7G/sR0uxXGkX2NwSVGnIz0ucVOBsbSl9PKd6A== +"@angular-devkit/build-angular@20.3.24": + version "20.3.24" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-20.3.24.tgz#871b7c134d9c9f77b16cd68bca340801638e56a7" + integrity sha512-zBjETGqKtiojF8VHvjELW+UAMBRJ7ziqNc1xwUtqRuWZQVz8HH6x8m8Ncw4mSq9ecCFC4VB7OWTJ1VkAdYSBnQ== dependencies: "@ampproject/remapping" "2.3.0" - "@angular-devkit/architect" "0.2003.22" - "@angular-devkit/build-webpack" "0.2003.22" - "@angular-devkit/core" "20.3.22" - "@angular/build" "20.3.22" + "@angular-devkit/architect" "0.2003.24" + "@angular-devkit/build-webpack" "0.2003.24" + "@angular-devkit/core" "20.3.24" + "@angular/build" "20.3.24" "@babel/core" "7.28.3" "@babel/generator" "7.28.3" "@babel/helper-annotate-as-pure" "7.27.3" @@ -188,14 +188,14 @@ "@babel/preset-env" "7.28.3" "@babel/runtime" "7.28.3" "@discoveryjs/json-ext" "0.6.3" - "@ngtools/webpack" "20.3.22" + "@ngtools/webpack" "20.3.24" ansi-colors "4.1.3" autoprefixer "10.4.21" babel-loader "10.0.0" browserslist "^4.21.5" copy-webpack-plugin "14.0.0" css-loader "7.1.2" - esbuild-wasm "0.25.9" + esbuild-wasm "0.28.0" fast-glob "3.3.3" http-proxy-middleware "3.0.5" istanbul-lib-instrument "6.0.3" @@ -228,20 +228,20 @@ webpack-merge "6.0.1" webpack-subresource-integrity "5.1.0" optionalDependencies: - esbuild "0.25.9" + esbuild "0.28.0" -"@angular-devkit/build-webpack@0.2003.22": - version "0.2003.22" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.2003.22.tgz#9769c91cfd0203b1136cdd6b6ed4d205e17da70b" - integrity sha512-ad4iW5CDGDYFXR9ZpdY8O2n4mx+tJtjx4mfC9Z+0ikwA4ir2IW2BOUhWvJ6IDq3f7kulFxP8S4zWN9r7jsZw3A== +"@angular-devkit/build-webpack@0.2003.24": + version "0.2003.24" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.2003.24.tgz#9d96b1d2c05faf3bcb696059c736d9498230ba36" + integrity sha512-gfadsLK3SxbGpZQMSJGr8RK65R2mZi/VmHXdztXQHATvMpy+00CX1Nu3n+lPTxOVbC73aIczUkUbIuRC1HBCqg== dependencies: - "@angular-devkit/architect" "0.2003.22" + "@angular-devkit/architect" "0.2003.24" rxjs "7.8.2" -"@angular-devkit/core@20.3.22", "@angular-devkit/core@>= 20.0.0 < 21.0.0", "@angular-devkit/core@^20.0.0": - version "20.3.22" - resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-20.3.22.tgz#6040c5673c4f4bb2eaaaac0bb9801ea6fa0886ea" - integrity sha512-1vZnZTAjGcCM+86v2al+2eiROiSw0uAWeVllfHSQe0KsKOP1FE8UUUiWChhxVn7vIxypphlfGunkeeIn1C/ZFw== +"@angular-devkit/core@20.3.24", "@angular-devkit/core@>= 20.0.0 < 21.0.0", "@angular-devkit/core@^20.0.0": + version "20.3.24" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-20.3.24.tgz#e4108ad599d507d934b9ca990d98a4bb23bd0471" + integrity sha512-kmOjXJcbFxUI91nds9n6XZ6Y/DyQ7/TqRXbHHqvkz9RtlIpdbgWHlIZIq6mgsPOgPBzkxFjtncVARYZUI3yxaw== dependencies: ajv "8.18.0" ajv-formats "3.0.1" @@ -250,12 +250,12 @@ rxjs "7.8.2" source-map "0.7.6" -"@angular-devkit/schematics@20.3.22", "@angular-devkit/schematics@>= 20.0.0 < 21.0.0": - version "20.3.22" - resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-20.3.22.tgz#e1aab6eaa431f323db2425af05276dbe45c63fc2" - integrity sha512-gN2XSXRn3eErGEJlH0iSfQZZ7NdxVZNdjSxuVEGBEFhe3cVeC21LzM3GTWW6xwtBb4pxHglFyc7BUFiYtZiYtg== +"@angular-devkit/schematics@20.3.24", "@angular-devkit/schematics@>= 20.0.0 < 21.0.0": + version "20.3.24" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-20.3.24.tgz#4631ec0755363173db0e89eadc0d756985e0bae1" + integrity sha512-B5fBPi0xnEDI0wLLkCjsrYjazRPyf+rnHLAHi34thMdeY9dqljJGWYdNuyUUBak6HNPBLdEo1EUSNcOF9OWt4A== dependencies: - "@angular-devkit/core" "20.3.22" + "@angular-devkit/core" "20.3.24" jsonc-parser "3.3.1" magic-string "0.30.17" ora "8.2.0" @@ -321,20 +321,20 @@ dependencies: "@angular-eslint/bundled-angular-compiler" "20.7.0" -"@angular/animations@20.3.18": - version "20.3.18" - resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-20.3.18.tgz#e675e045839d559b4917053eb0f74313bf3235ef" - integrity sha512-XFxgSyjfs0SRD2vQVFJljmM4z9nTvUoI8TRqSre/+l8D2FgzD5pG67Aj2BgDgpSFAUkIcI37G48ijK7a3ZZ3WA== +"@angular/animations@20.3.19": + version "20.3.19" + resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-20.3.19.tgz#636cf19528c548427f3f2ce6c4dd3b863b1475c4" + integrity sha512-/FjU9i7J58/yBURhgVSIiLDcuyOfJxAa0b7ZrOsx6P+FES+M2T2BKZl5V2NuiP2fDFtjsV7U+M/Z9UNUmeHCEw== dependencies: tslib "^2.3.0" -"@angular/build@20.3.22", "@angular/build@^20.0.0": - version "20.3.22" - resolved "https://registry.yarnpkg.com/@angular/build/-/build-20.3.22.tgz#c29b7980a96f6353b2d1c5cc2d7bcaf82c46d181" - integrity sha512-sxjVZU6AZHXyKRHJUMawXOj4qMf3vm8XK6wUejr01UKj6BqW2YWaQO26RpRJssXD2ITTqn6+UBwL7pEwe2a4Jg== +"@angular/build@20.3.24", "@angular/build@^20.0.0": + version "20.3.24" + resolved "https://registry.yarnpkg.com/@angular/build/-/build-20.3.24.tgz#79e423a077a4177074b52e3a20c76f082fc08824" + integrity sha512-AMGXOr268y+kVutl4LpOXY2xv9P+RXLCyXUkzYwi8XwGyxAJZfyu/L5qtcO2llExp5CuvP0OxkWxk4JOGRi9TA== dependencies: "@ampproject/remapping" "2.3.0" - "@angular-devkit/architect" "0.2003.22" + "@angular-devkit/architect" "0.2003.24" "@babel/core" "7.28.3" "@babel/helper-annotate-as-pure" "7.27.3" "@babel/helper-split-export-declaration" "7.24.7" @@ -342,7 +342,7 @@ "@vitejs/plugin-basic-ssl" "2.1.0" beasties "0.3.5" browserslist "^4.23.0" - esbuild "0.25.9" + esbuild "0.28.0" https-proxy-agent "7.0.6" istanbul-lib-instrument "6.0.3" jsonc-parser "3.3.1" @@ -357,7 +357,7 @@ semver "7.7.2" source-map-support "0.5.21" tinyglobby "0.2.14" - vite "7.1.11" + vite "7.3.2" watchpack "2.4.4" optionalDependencies: lmdb "3.4.2" @@ -370,18 +370,18 @@ parse5 "^8.0.0" tslib "^2.3.0" -"@angular/cli@20.3.22": - version "20.3.22" - resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-20.3.22.tgz#ce00d20d55f458de39c80b080a749d4d03e4f9b6" - integrity sha512-0uyQPF0gGuzioWJKNyOzWSQrrC5GiidR+8gz1lODoJTnJZZdsP5n3nvccbcRmhy55B1WByHvQBE+6eDBbh06/g== +"@angular/cli@20.3.24": + version "20.3.24" + resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-20.3.24.tgz#e6ab456b83eede47ccc3503dd6068bdf04be4e05" + integrity sha512-TT7LldRJPCi1VGBJMzcrYU+P3w2G6zgubwhFUdJthUiS77A+At6WJqaRY2BdIS3l7HZOXTEaU2Vj2Gkf2ol2Yw== dependencies: - "@angular-devkit/architect" "0.2003.22" - "@angular-devkit/core" "20.3.22" - "@angular-devkit/schematics" "20.3.22" + "@angular-devkit/architect" "0.2003.24" + "@angular-devkit/core" "20.3.24" + "@angular-devkit/schematics" "20.3.24" "@inquirer/prompts" "7.8.2" "@listr2/prompt-adapter-inquirer" "3.0.1" "@modelcontextprotocol/sdk" "1.26.0" - "@schematics/angular" "20.3.22" + "@schematics/angular" "20.3.24" "@yarnpkg/lockfile" "1.1.0" algoliasearch "5.35.0" ini "5.0.0" @@ -394,17 +394,17 @@ yargs "18.0.0" zod "4.1.13" -"@angular/common@20.3.18": - version "20.3.18" - resolved "https://registry.yarnpkg.com/@angular/common/-/common-20.3.18.tgz#311dc0658be69f1368db2eeea92ea1b940329975" - integrity sha512-M62oQbSTRmnGavIVCwimoadg/PDWadgNhactMm9fgH0eM9rx+iWBAYJk4VufO0bwOhysFpRZpJgXlFjOifz/Jw== +"@angular/common@20.3.19": + version "20.3.19" + resolved "https://registry.yarnpkg.com/@angular/common/-/common-20.3.19.tgz#fb19c335a5a5ea84cd6c90a4abda99c2de5c8b17" + integrity sha512-hcB1eUEN8LGcKGc4DlRJ+abS6AYfbEHDZKg8LnXNugkbwI6Ebyh2AUYTDhzZL2S4aH+C8biHKgSYHFCqieCRhA== dependencies: tslib "^2.3.0" -"@angular/compiler-cli@20.3.18": - version "20.3.18" - resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-20.3.18.tgz#0f4726d1624c9def0f60c65f60e61a226676459c" - integrity sha512-zsoEgLgnblmRbi47YwMghKirJ8IBKJ3+I8TxLBRIBrhx+KHFp+6oeDeLyu9H+djdyk88zexVd09wzR/YK73F0g== +"@angular/compiler-cli@20.3.19": + version "20.3.19" + resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-20.3.19.tgz#baeb020e693a759b0b8190931611309678b1e011" + integrity sha512-ET/JjO8s62kAHfgIsGXlvW5VUwLqHm03q1y/2yD7aQW/WdDvssMsvZv7Knl440989vdOFemIGTMwVPakmWqRmA== dependencies: "@babel/core" "7.28.3" "@jridgewell/sourcemap-codec" "^1.4.14" @@ -415,31 +415,31 @@ tslib "^2.3.0" yargs "^18.0.0" -"@angular/compiler@20.3.18": - version "20.3.18" - resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-20.3.18.tgz#5370dc1a24d55623828a2b0875c776c8da13fdc6" - integrity sha512-AaP/LCiDNcYmF135EEozjyR04NRBT38ZfBHQwjhgwiBBTejmvcpHwJaHSkraLpZqZzE4BQqqmgiQ1EJqxEwLVA== +"@angular/compiler@20.3.19": + version "20.3.19" + resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-20.3.19.tgz#cce87072c55c1bab3fda92d83048d0136cea83ad" + integrity sha512-ETkgDKm0l2PuaBubgPJe0ccy8kE75DFu6/zKcz7TUuk3KrKF2OZAopbbjftsUSZGeCNvCdqHzjmcL6hQ6oAOwA== dependencies: tslib "^2.3.0" -"@angular/core@20.3.18": - version "20.3.18" - resolved "https://registry.yarnpkg.com/@angular/core/-/core-20.3.18.tgz#af91805841184cd87e862134a806bba019d170ea" - integrity sha512-B+NQQngd/aDbcfW0zGLis3wTLDeHTeTYMl/mGKQH+HwdPaRCKI1wEtaXaOYVJXkP2FeThocPevB8gLwNlPQUUw== +"@angular/core@20.3.19": + version "20.3.19" + resolved "https://registry.yarnpkg.com/@angular/core/-/core-20.3.19.tgz#ce8b2a0df48612b3d9bfefe8098a1018a3186406" + integrity sha512-SYnwW+q51bQoPtGFoGovm1P5GK9fMEXsG0lGaEAUapjskblAYyX7hLlM/jgueSojv2SjhqNF8aXR+gjHLhZVNA== dependencies: tslib "^2.3.0" -"@angular/forms@20.3.18": - version "20.3.18" - resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-20.3.18.tgz#ebd249a381dd3f6e5af3f760d7c3a8a6a76531ac" - integrity sha512-x6/99LfxolyZIFUL3Wr0OrtuXHEDwEz/rwx+WzE7NL+n35yO40t3kp0Sn5uMFwI94i91QZJmXHltMpZhrVLuYg== +"@angular/forms@20.3.19": + version "20.3.19" + resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-20.3.19.tgz#179fa22f6e2daf3fe58c400d3e3a7c10647f0ebc" + integrity sha512-WJotd+Lhl4FG2b0K+aQNyQDHhR515zKCuphjiUqEW7sifWrOQxANLKzPBngGrH75ayANFgPaDf7U3ZRIoblcQA== dependencies: tslib "^2.3.0" -"@angular/language-service@20.3.18": - version "20.3.18" - resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-20.3.18.tgz#b0c8c9bcf085907765e3cb7fbf0e14e79259e3c0" - integrity sha512-V1ZBqeTtZYH9H8/G1qCw6gafsJmhMIMFjLX0Hv2KpTpmfK9nxIHPEVnshr3xT+qKYJIrMV/cU5YOzInEapLpuQ== +"@angular/language-service@20.3.19": + version "20.3.19" + resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-20.3.19.tgz#ba9e04a35717d948ff972476486eae2ea25a109d" + integrity sha512-9J0XrAKXInz11KKyNMrMZmn2NSjVbxzt/DsAumbrzzixeZwiY7vDy2Kqw/LLFLi7IlfMQ/gznz/mCVVgUWI5Gg== "@angular/material@20.2.14": version "20.2.14" @@ -448,24 +448,24 @@ dependencies: tslib "^2.3.0" -"@angular/platform-browser-dynamic@20.3.18": - version "20.3.18" - resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.3.18.tgz#c4c633fcc4782e1a361a0904d2bc3f180bbf0f81" - integrity sha512-NyTobOGYVzGmPmtI+3lxMzxi0TbLq4SRNQ2ENEJAt6k2JnMmHBm483ppLRAM47nGlDdiraW0IX93EtYYNkiK3g== +"@angular/platform-browser-dynamic@20.3.19": + version "20.3.19" + resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.3.19.tgz#b13cb94dc39be24e22d70712605ca879aa5e83b2" + integrity sha512-OgErw7wjcC+8yKF5h99hJq8x+tvc091wThfmdL5YC+U3HgRmUaNZFgB/jR7cb/NeeeC42QW5Vc0qoUTC9rMnLQ== dependencies: tslib "^2.3.0" -"@angular/platform-browser@20.3.18": - version "20.3.18" - resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-20.3.18.tgz#33b5d5cdddc4a0f60a9ddf886b941587a348d304" - integrity sha512-q6s5rEN1yYazpHYp+k4pboXRzMsRB9auzTRBEhyXSGYxqzrnn3qHN0DqgsLC9WAdyhCgnIEMFA8kRT+W277DqQ== +"@angular/platform-browser@20.3.19": + version "20.3.19" + resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-20.3.19.tgz#4bffa36591d9ba5adc89a1b48952fdfd982d2f30" + integrity sha512-TRZfatH1B/kreDwFRwtpLEurJQ6044qh6DWpvxzTbugaG5otLQJKTk+1z81/KsJwQqc1+24v+yuywc1LM7aq7w== dependencies: tslib "^2.3.0" -"@angular/router@20.3.18": - version "20.3.18" - resolved "https://registry.yarnpkg.com/@angular/router/-/router-20.3.18.tgz#276fe53975ceb858f4e76405f8219fbe5cb20204" - integrity sha512-3CWejsEYr+ze+ktvWN/qHdyq5WLrj96QZpGYJyxh1pchIcpMPE9MmLpdjf0CUrWYB7g/85u0Geq/xsz72JrGng== +"@angular/router@20.3.19": + version "20.3.19" + resolved "https://registry.yarnpkg.com/@angular/router/-/router-20.3.19.tgz#2346ff945a8039194ba921a473a8f4565a92b949" + integrity sha512-qHrMniHOsCJ4neZmcQVodjutJilyXAXk7EhLa931QyL0qyVKVomv6E0I3UFzRaC3ZeHc+hzBdU6C6bvMFKTl1g== dependencies: tslib "^2.3.0" @@ -1389,135 +1389,135 @@ resolved "https://registry.yarnpkg.com/@es-joy/resolve.exports/-/resolve.exports-1.2.0.tgz#fe541a68aa080255f798c8561714ac8fad72cdd5" integrity sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g== -"@esbuild/aix-ppc64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz#bef96351f16520055c947aba28802eede3c9e9a9" - integrity sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA== - -"@esbuild/android-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz#d2e70be7d51a529425422091e0dcb90374c1546c" - integrity sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg== - -"@esbuild/android-arm@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.9.tgz#d2a753fe2a4c73b79437d0ba1480e2d760097419" - integrity sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ== - -"@esbuild/android-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.9.tgz#5278836e3c7ae75761626962f902a0d55352e683" - integrity sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw== - -"@esbuild/darwin-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz#f1513eaf9ec8fa15dcaf4c341b0f005d3e8b47ae" - integrity sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg== - -"@esbuild/darwin-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz#e27dbc3b507b3a1cea3b9280a04b8b6b725f82be" - integrity sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ== - -"@esbuild/freebsd-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz#364e3e5b7a1fd45d92be08c6cc5d890ca75908ca" - integrity sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q== - -"@esbuild/freebsd-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz#7c869b45faeb3df668e19ace07335a0711ec56ab" - integrity sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg== - -"@esbuild/linux-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz#48d42861758c940b61abea43ba9a29b186d6cb8b" - integrity sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw== - -"@esbuild/linux-arm@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz#6ce4b9cabf148274101701d112b89dc67cc52f37" - integrity sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw== - -"@esbuild/linux-ia32@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz#207e54899b79cac9c26c323fc1caa32e3143f1c4" - integrity sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A== - -"@esbuild/linux-loong64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz#0ba48a127159a8f6abb5827f21198b999ffd1fc0" - integrity sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ== - -"@esbuild/linux-mips64el@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz#a4d4cc693d185f66a6afde94f772b38ce5d64eb5" - integrity sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA== - -"@esbuild/linux-ppc64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz#0f5805c1c6d6435a1dafdc043cb07a19050357db" - integrity sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w== - -"@esbuild/linux-riscv64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz#6776edece0f8fca79f3386398b5183ff2a827547" - integrity sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg== - -"@esbuild/linux-s390x@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz#3f6f29ef036938447c2218d309dc875225861830" - integrity sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA== - -"@esbuild/linux-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz#831fe0b0e1a80a8b8391224ea2377d5520e1527f" - integrity sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg== - -"@esbuild/netbsd-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz#06f99d7eebe035fbbe43de01c9d7e98d2a0aa548" - integrity sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q== - -"@esbuild/netbsd-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz#db99858e6bed6e73911f92a88e4edd3a8c429a52" - integrity sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g== - -"@esbuild/openbsd-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz#afb886c867e36f9d86bb21e878e1185f5d5a0935" - integrity sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ== - -"@esbuild/openbsd-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz#30855c9f8381fac6a0ef5b5f31ac6e7108a66ecf" - integrity sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA== - -"@esbuild/openharmony-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz#2f2144af31e67adc2a8e3705c20c2bd97bd88314" - integrity sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg== - -"@esbuild/sunos-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz#69b99a9b5bd226c9eb9c6a73f990fddd497d732e" - integrity sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw== - -"@esbuild/win32-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz#d789330a712af916c88325f4ffe465f885719c6b" - integrity sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ== - -"@esbuild/win32-ia32@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz#52fc735406bd49688253e74e4e837ac2ba0789e3" - integrity sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww== - -"@esbuild/win32-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz#585624dc829cfb6e7c0aa6c3ca7d7e6daa87e34f" - integrity sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ== +"@esbuild/aix-ppc64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz#7a289c158e29cbf59ea0afc83cc80f06d1c89402" + integrity sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA== + +"@esbuild/android-arm64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz#b8828d9edfa3a92660644eb8de6e4f3c203d7b17" + integrity sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw== + +"@esbuild/android-arm@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.28.0.tgz#5ec1847605e05b5dbe5df90db9ff7e3e4c58dca7" + integrity sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ== + +"@esbuild/android-x64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.28.0.tgz#390642175b88ef82bad4cce03f8ab13fe9b1912e" + integrity sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA== + +"@esbuild/darwin-arm64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz#ae45325960d5950cd6951e4f97396f4e1ff7d8d3" + integrity sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q== + +"@esbuild/darwin-x64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz#c079247d589b6b99449659d94f06951b84bff2e4" + integrity sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ== + +"@esbuild/freebsd-arm64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz#45c456215a486593c94900297202dc11c880a37a" + integrity sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q== + +"@esbuild/freebsd-x64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz#0399494c1c85e4388e9b7040bd60d48f2a5b0d2c" + integrity sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw== + +"@esbuild/linux-arm64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz#d6d9f09ef0de54116bf459a4d53cac7e0952fe39" + integrity sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A== + +"@esbuild/linux-arm@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz#7b42ffa84c288ae94fdc431c1b28a89e3c3b9278" + integrity sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw== + +"@esbuild/linux-ia32@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz#deb15d112ed8dd605346b6b953d23a21ff81253f" + integrity sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ== + +"@esbuild/linux-loong64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz#81fb89d07eecc79b157dea61033757726fce0ca4" + integrity sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg== + +"@esbuild/linux-mips64el@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz#d0e42691b3ff7af9fb2217b70fc01f343bdb62bb" + integrity sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w== + +"@esbuild/linux-ppc64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz#389f3e5e98f17d477c467cc87136e1a076eead87" + integrity sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg== + +"@esbuild/linux-riscv64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz#763bd60d59b242be12da1e67d5729f3024c605fa" + integrity sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ== + +"@esbuild/linux-s390x@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz#aac6061634872e4677de693bce8030d73b1fd055" + integrity sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q== + +"@esbuild/linux-x64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz#4f2917747188fe77632bcec65b2d84b422419779" + integrity sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ== + +"@esbuild/netbsd-arm64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz#814df0ae57a0c386814491b8397eeba82094a947" + integrity sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw== + +"@esbuild/netbsd-x64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz#e01bdf7e60fa1a08e46d46d960b0d9bb8ac210af" + integrity sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw== + +"@esbuild/openbsd-arm64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz#4a15c36aacca68d2d5a4c90b710c06759f4c1ffa" + integrity sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g== + +"@esbuild/openbsd-x64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz#475e6101498a8ecce3008d7c388111d7a27c17bd" + integrity sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA== + +"@esbuild/openharmony-arm64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz#cfdc3957f0b7a69f1bde129aad17fcc2f6fa033e" + integrity sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w== + +"@esbuild/sunos-x64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz#a013c856fecacd1c3aec985c8afe1d1cb017497d" + integrity sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw== + +"@esbuild/win32-arm64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz#eae05e0f35271cad3898b43168d3e9a3bbaf47e5" + integrity sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA== + +"@esbuild/win32-ia32@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz#06161ebc5bf75c08d69feb3c6b22560515913998" + integrity sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA== + +"@esbuild/win32-x64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz#04d90d5752b4ce65d2b6ac25eba08ff7624fe07c" + integrity sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw== "@eslint-community/eslint-utils@^4.8.0", "@eslint-community/eslint-utils@^4.9.1": version "4.9.1" @@ -2397,10 +2397,10 @@ dependencies: tslib "^2.0.0" -"@ngtools/webpack@20.3.22": - version "20.3.22" - resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-20.3.22.tgz#65c7eefb187516f00d2268c60613294cf8d55028" - integrity sha512-EgOiRjYpNG5Mu/WAhMwQrAB1BBwWrApbYm2hTU9KjaxOZvtWHjeFfsiULgd1T76GpiF4t3Iw1GtNljzgD6fT/A== +"@ngtools/webpack@20.3.24": + version "20.3.24" + resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-20.3.24.tgz#8e344ebaea46d7e669220f02a33bb61826bb35f6" + integrity sha512-PIP9hFVF6OOmDxG0s6vX7cHm/6wwWK8jXd6e1I/CewR0zpVPtR1vxhhw9CrY4VEUCFSL6x2NuW/U3cI7LU+Z1Q== "@ngx-translate/core@^17.0.0": version "17.0.0" @@ -2751,13 +2751,13 @@ resolved "https://registry.yarnpkg.com/@sanity/diff-match-patch/-/diff-match-patch-3.2.0.tgz#7ce587273f7372a146308cb1075ba26177d42cdb" integrity sha512-4hPADs0qUThFZkBK/crnfKKHg71qkRowfktBljH2UIxGHHTxIzt8g8fBiXItyCjxkuNy+zpYOdRMifQNv8+Yww== -"@schematics/angular@20.3.22": - version "20.3.22" - resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-20.3.22.tgz#acd7a990148a99f04793ee2e92a56694667790a4" - integrity sha512-wXTdFaPIBnSSNj/m0kclvPCYQOc2EGTQN1+Q3j9RIghS9gKgPxI1unSfgieJldZWKzcl8+WdB2zUuDzE7tEshQ== +"@schematics/angular@20.3.24": + version "20.3.24" + resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-20.3.24.tgz#1cef967a08a55336b296dfd0e2c5207dc1c43226" + integrity sha512-GNB8zI8Lz0rJl4Q7FH4Y8ZmRpODkNDKGxWObfZ39POgiyr3CtT5sMRTQq1lWRWTlZeV8uD51DvW/EsAsbaS4HA== dependencies: - "@angular-devkit/core" "20.3.22" - "@angular-devkit/schematics" "20.3.22" + "@angular-devkit/core" "20.3.24" + "@angular-devkit/schematics" "20.3.24" jsonc-parser "3.3.1" "@sigstore/bundle@^4.0.0": @@ -5513,42 +5513,42 @@ es-to-primitive@^1.3.0: is-date-object "^1.0.5" is-symbol "^1.0.4" -esbuild-wasm@0.25.9: - version "0.25.9" - resolved "https://registry.yarnpkg.com/esbuild-wasm/-/esbuild-wasm-0.25.9.tgz#70e15ff86d6d3e55b0e10817c826783f7ff6612a" - integrity sha512-Jpv5tCSwQg18aCqCRD3oHIX/prBhXMDapIoG//A+6+dV0e7KQMGFg85ihJ5T1EeMjbZjON3TqFy0VrGAnIHLDA== +esbuild-wasm@0.28.0: + version "0.28.0" + resolved "https://registry.yarnpkg.com/esbuild-wasm/-/esbuild-wasm-0.28.0.tgz#e2e60b86ed19e0f6f5a1e6c1c722f5a811d7d5f7" + integrity sha512-5TRVKExcEmeMkccIZMzUq+Az6X2RoMAJyfl6SMMO1dMVhmvt0I2mx7gAb6zYi42n4d1ETcatFXazGKzA+aW7fg== -esbuild@0.25.9, esbuild@^0.25.0: - version "0.25.9" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.9.tgz#15ab8e39ae6cdc64c24ff8a2c0aef5b3fd9fa976" - integrity sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g== +esbuild@0.28.0, esbuild@^0.27.0: + version "0.28.0" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.28.0.tgz#5dee347ffb3e3874212a35a69836b077b1ce6d96" + integrity sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw== optionalDependencies: - "@esbuild/aix-ppc64" "0.25.9" - "@esbuild/android-arm" "0.25.9" - "@esbuild/android-arm64" "0.25.9" - "@esbuild/android-x64" "0.25.9" - "@esbuild/darwin-arm64" "0.25.9" - "@esbuild/darwin-x64" "0.25.9" - "@esbuild/freebsd-arm64" "0.25.9" - "@esbuild/freebsd-x64" "0.25.9" - "@esbuild/linux-arm" "0.25.9" - "@esbuild/linux-arm64" "0.25.9" - "@esbuild/linux-ia32" "0.25.9" - "@esbuild/linux-loong64" "0.25.9" - "@esbuild/linux-mips64el" "0.25.9" - "@esbuild/linux-ppc64" "0.25.9" - "@esbuild/linux-riscv64" "0.25.9" - "@esbuild/linux-s390x" "0.25.9" - "@esbuild/linux-x64" "0.25.9" - "@esbuild/netbsd-arm64" "0.25.9" - "@esbuild/netbsd-x64" "0.25.9" - "@esbuild/openbsd-arm64" "0.25.9" - "@esbuild/openbsd-x64" "0.25.9" - "@esbuild/openharmony-arm64" "0.25.9" - "@esbuild/sunos-x64" "0.25.9" - "@esbuild/win32-arm64" "0.25.9" - "@esbuild/win32-ia32" "0.25.9" - "@esbuild/win32-x64" "0.25.9" + "@esbuild/aix-ppc64" "0.28.0" + "@esbuild/android-arm" "0.28.0" + "@esbuild/android-arm64" "0.28.0" + "@esbuild/android-x64" "0.28.0" + "@esbuild/darwin-arm64" "0.28.0" + "@esbuild/darwin-x64" "0.28.0" + "@esbuild/freebsd-arm64" "0.28.0" + "@esbuild/freebsd-x64" "0.28.0" + "@esbuild/linux-arm" "0.28.0" + "@esbuild/linux-arm64" "0.28.0" + "@esbuild/linux-ia32" "0.28.0" + "@esbuild/linux-loong64" "0.28.0" + "@esbuild/linux-mips64el" "0.28.0" + "@esbuild/linux-ppc64" "0.28.0" + "@esbuild/linux-riscv64" "0.28.0" + "@esbuild/linux-s390x" "0.28.0" + "@esbuild/linux-x64" "0.28.0" + "@esbuild/netbsd-arm64" "0.28.0" + "@esbuild/netbsd-x64" "0.28.0" + "@esbuild/openbsd-arm64" "0.28.0" + "@esbuild/openbsd-x64" "0.28.0" + "@esbuild/openharmony-arm64" "0.28.0" + "@esbuild/sunos-x64" "0.28.0" + "@esbuild/win32-arm64" "0.28.0" + "@esbuild/win32-ia32" "0.28.0" + "@esbuild/win32-x64" "0.28.0" escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" @@ -10397,12 +10397,12 @@ vary@^1, vary@^1.1.2, vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== -vite@7.1.11: - version "7.1.11" - resolved "https://registry.yarnpkg.com/vite/-/vite-7.1.11.tgz#4d006746112fee056df64985191e846ebfb6007e" - integrity sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg== +vite@7.3.2: + version "7.3.2" + resolved "https://registry.yarnpkg.com/vite/-/vite-7.3.2.tgz#cb041794d4c1395e28baea98198fd6e8f4b96b5c" + integrity sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg== dependencies: - esbuild "^0.25.0" + esbuild "^0.27.0" fdir "^6.5.0" picomatch "^4.0.3" postcss "^8.5.6" From 0765d1f66231341bb39cb422a7850c3ec6264b0a Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 21 Apr 2026 11:07:44 +0300 Subject: [PATCH 17/25] Fixed CVE-2026-4800 --- ui-ngx/yarn.lock | 579 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 391 insertions(+), 188 deletions(-) diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index ec9183bcc7..454822d186 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -469,18 +469,13 @@ dependencies: tslib "^2.3.0" -"@antfu/install-pkg@^0.4.0": - version "0.4.1" - resolved "https://registry.yarnpkg.com/@antfu/install-pkg/-/install-pkg-0.4.1.tgz#d1d7f3be96ecdb41581629cafe8626d1748c0cf1" - integrity sha512-T7yB5QNG29afhWVkVq7XeIMBa5U/vs9mX69YqayXypPRmYzUmzwnYltplHmPtZ4HPCn+sQKeXW8I47wCbuBOjw== +"@antfu/install-pkg@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@antfu/install-pkg/-/install-pkg-1.1.0.tgz#78fa036be1a6081b5a77a5cf59f50c7752b6ba26" + integrity sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ== dependencies: - package-manager-detector "^0.2.0" - tinyexec "^0.3.0" - -"@antfu/utils@^0.7.10": - version "0.7.10" - resolved "https://registry.yarnpkg.com/@antfu/utils/-/utils-0.7.10.tgz#ae829f170158e297a9b6a28f161a8e487d00814d" - integrity sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww== + package-manager-detector "^1.3.0" + tinyexec "^1.0.1" "@auth0/angular-jwt@^5.2.0": version "5.2.0" @@ -1324,42 +1319,40 @@ "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.28.5" -"@braintree/sanitize-url@^7.0.1": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-7.1.0.tgz#048e48aab4f1460e3121e22aa62459d16653dc85" - integrity sha512-o+UlMLt49RvtCASlOMW0AkHnabN9wR9rwCCherxO0yG4Npy34GkvrAqdXQvrhNs+jh+gkK8gB8Lf05qL/O7KWg== +"@braintree/sanitize-url@^7.1.1": + version "7.1.2" + resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz#ca2035b0fefe956a8676ff0c69af73e605fcd81f" + integrity sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA== -"@chevrotain/cst-dts-gen@11.0.3": - version "11.0.3" - resolved "https://registry.yarnpkg.com/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz#5e0863cc57dc45e204ccfee6303225d15d9d4783" - integrity sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ== +"@chevrotain/cst-dts-gen@12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@chevrotain/cst-dts-gen/-/cst-dts-gen-12.0.0.tgz#ec068e1e83c5fdad69d81773556cae97f0b5dcdb" + integrity sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg== dependencies: - "@chevrotain/gast" "11.0.3" - "@chevrotain/types" "11.0.3" - lodash-es "4.17.21" + "@chevrotain/gast" "12.0.0" + "@chevrotain/types" "12.0.0" -"@chevrotain/gast@11.0.3": - version "11.0.3" - resolved "https://registry.yarnpkg.com/@chevrotain/gast/-/gast-11.0.3.tgz#e84d8880323fe8cbe792ef69ce3ffd43a936e818" - integrity sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q== +"@chevrotain/gast@12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@chevrotain/gast/-/gast-12.0.0.tgz#0e0cbf8eee01c7a4449b9caf19e5f3834dba2c35" + integrity sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ== dependencies: - "@chevrotain/types" "11.0.3" - lodash-es "4.17.21" + "@chevrotain/types" "12.0.0" -"@chevrotain/regexp-to-ast@11.0.3": - version "11.0.3" - resolved "https://registry.yarnpkg.com/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz#11429a81c74a8e6a829271ce02fc66166d56dcdb" - integrity sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA== +"@chevrotain/regexp-to-ast@12.0.0", "@chevrotain/regexp-to-ast@~12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@chevrotain/regexp-to-ast/-/regexp-to-ast-12.0.0.tgz#a90bc4b4f5337a883a88dddd0cca7c38cfe66a7a" + integrity sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA== -"@chevrotain/types@11.0.3": - version "11.0.3" - resolved "https://registry.yarnpkg.com/@chevrotain/types/-/types-11.0.3.tgz#f8a03914f7b937f594f56eb89312b3b8f1c91848" - integrity sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ== +"@chevrotain/types@12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@chevrotain/types/-/types-12.0.0.tgz#a762b5c2b4f07496b56c93c30ce224b3637cc2c8" + integrity sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA== -"@chevrotain/utils@11.0.3": - version "11.0.3" - resolved "https://registry.yarnpkg.com/@chevrotain/utils/-/utils-11.0.3.tgz#e39999307b102cff3645ec4f5b3665f5297a2224" - integrity sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ== +"@chevrotain/utils@12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@chevrotain/utils/-/utils-12.0.0.tgz#9aab2055df43d0bb55919eaca76a9cda45e52b89" + integrity sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA== "@cspotcode/source-map-support@^0.8.0": version "0.8.1" @@ -1649,18 +1642,14 @@ resolved "https://registry.yarnpkg.com/@iconify/types/-/types-2.0.0.tgz#ab0e9ea681d6c8a1214f30cd741fe3a20cc57f57" integrity sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg== -"@iconify/utils@^2.1.32": - version "2.1.33" - resolved "https://registry.yarnpkg.com/@iconify/utils/-/utils-2.1.33.tgz#cbf7242a52fd0ec58c42d37d28e4406b5327e8c0" - integrity sha512-jP9h6v/g0BIZx0p7XGJJVtkVnydtbgTgt9mVNcGDYwaa7UhdHdI9dvoq+gKj9sijMSJKxUPEG2JyjsgXjxL7Kw== +"@iconify/utils@^3.0.2": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@iconify/utils/-/utils-3.1.0.tgz#fb41882915f97fee6f91a2fbb8263e8772ca0438" + integrity sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw== dependencies: - "@antfu/install-pkg" "^0.4.0" - "@antfu/utils" "^0.7.10" + "@antfu/install-pkg" "^1.1.0" "@iconify/types" "^2.0.0" - debug "^4.3.6" - kolorist "^1.8.0" - local-pkg "^0.5.0" - mlly "^1.7.1" + mlly "^1.8.0" "@inquirer/ansi@^1.0.2": version "1.0.2" @@ -2165,12 +2154,12 @@ resolved "https://registry.yarnpkg.com/@mdi/svg/-/svg-7.4.47.tgz#f8e5516aae129764a76d1bb2f27e55bee03e6e90" integrity sha512-WQ2gDll12T9WD34fdRFgQVgO8bag3gavrAgJ0frN4phlwdJARpE6gO1YvLEMJR0KKgoc+/Ea/A0Pp11I00xBvw== -"@mermaid-js/parser@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@mermaid-js/parser/-/parser-0.3.0.tgz#7a28714599f692f93df130b299fa1aadc9f9c8ab" - integrity sha512-HsvL6zgE5sUPGgkIDlmAWR1HTNHz2Iy11BAWPTa4Jjabkpguy4Ze2gzfLrg6pdRuBvFwgUYyxiaNqZwrEEXepA== +"@mermaid-js/parser@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@mermaid-js/parser/-/parser-1.1.0.tgz#8f96c35ddab34a1b12af58f2c59f5abb7d4743fc" + integrity sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw== dependencies: - langium "3.0.0" + langium "^4.0.0" "@messageformat/core@^3.4.0": version "3.4.0" @@ -3087,6 +3076,216 @@ dependencies: "@types/node" "*" +"@types/d3-array@*": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.2.tgz#e02151464d02d4a1b44646d0fcdb93faf88fde8c" + integrity sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw== + +"@types/d3-axis@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-3.0.6.tgz#e760e5765b8188b1defa32bc8bb6062f81e4c795" + integrity sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-brush@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-3.0.6.tgz#c2f4362b045d472e1b186cdbec329ba52bdaee6c" + integrity sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-chord@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-3.0.6.tgz#1706ca40cf7ea59a0add8f4456efff8f8775793d" + integrity sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg== + +"@types/d3-color@*": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" + integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== + +"@types/d3-contour@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-3.0.6.tgz#9ada3fa9c4d00e3a5093fed0356c7ab929604231" + integrity sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg== + dependencies: + "@types/d3-array" "*" + "@types/geojson" "*" + +"@types/d3-delaunay@*": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz#185c1a80cc807fdda2a3fe960f7c11c4a27952e1" + integrity sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw== + +"@types/d3-dispatch@*": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz#ef004d8a128046cfce434d17182f834e44ef95b2" + integrity sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA== + +"@types/d3-drag@*": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.7.tgz#b13aba8b2442b4068c9a9e6d1d82f8bcea77fc02" + integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-dsv@*": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz#0a351f996dc99b37f4fa58b492c2d1c04e3dac17" + integrity sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g== + +"@types/d3-ease@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b" + integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== + +"@types/d3-fetch@*": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz#c04a2b4f23181aa376f30af0283dbc7b3b569980" + integrity sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA== + dependencies: + "@types/d3-dsv" "*" + +"@types/d3-force@*": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-3.0.10.tgz#6dc8fc6e1f35704f3b057090beeeb7ac674bff1a" + integrity sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw== + +"@types/d3-format@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.4.tgz#b1e4465644ddb3fdf3a263febb240a6cd616de90" + integrity sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g== + +"@types/d3-geo@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-3.1.0.tgz#b9e56a079449174f0a2c8684a9a4df3f60522440" + integrity sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ== + dependencies: + "@types/geojson" "*" + +"@types/d3-hierarchy@*": + version "3.1.7" + resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz#6023fb3b2d463229f2d680f9ac4b47466f71f17b" + integrity sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg== + +"@types/d3-interpolate@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" + integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== + dependencies: + "@types/d3-color" "*" + +"@types/d3-path@*": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.1.tgz#f632b380c3aca1dba8e34aa049bcd6a4af23df8a" + integrity sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg== + +"@types/d3-polygon@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz#dfae54a6d35d19e76ac9565bcb32a8e54693189c" + integrity sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA== + +"@types/d3-quadtree@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz#d4740b0fe35b1c58b66e1488f4e7ed02952f570f" + integrity sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg== + +"@types/d3-random@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-3.0.3.tgz#ed995c71ecb15e0cd31e22d9d5d23942e3300cfb" + integrity sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ== + +"@types/d3-scale-chromatic@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz#dc6d4f9a98376f18ea50bad6c39537f1b5463c39" + integrity sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ== + +"@types/d3-scale@*": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.9.tgz#57a2f707242e6fe1de81ad7bfcccaaf606179afb" + integrity sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw== + dependencies: + "@types/d3-time" "*" + +"@types/d3-selection@*": + version "3.0.11" + resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.11.tgz#bd7a45fc0a8c3167a631675e61bc2ca2b058d4a3" + integrity sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w== + +"@types/d3-shape@*": + version "3.1.8" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.8.tgz#d1516cc508753be06852cd06758e3bb54a22b0e3" + integrity sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time-format@*": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz#d6bc1e6b6a7db69cccfbbdd4c34b70632d9e9db2" + integrity sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg== + +"@types/d3-time@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.4.tgz#8472feecd639691450dd8000eb33edd444e1323f" + integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g== + +"@types/d3-timer@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" + integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== + +"@types/d3-transition@*": + version "3.0.9" + resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.9.tgz#1136bc57e9ddb3c390dccc9b5ff3b7d2b8d94706" + integrity sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-zoom@*": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz#dccb32d1c56b1e1c6e0f1180d994896f038bc40b" + integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw== + dependencies: + "@types/d3-interpolate" "*" + "@types/d3-selection" "*" + +"@types/d3@^7.4.3": + version "7.4.3" + resolved "https://registry.yarnpkg.com/@types/d3/-/d3-7.4.3.tgz#d4550a85d08f4978faf0a4c36b848c61eaac07e2" + integrity sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww== + dependencies: + "@types/d3-array" "*" + "@types/d3-axis" "*" + "@types/d3-brush" "*" + "@types/d3-chord" "*" + "@types/d3-color" "*" + "@types/d3-contour" "*" + "@types/d3-delaunay" "*" + "@types/d3-dispatch" "*" + "@types/d3-drag" "*" + "@types/d3-dsv" "*" + "@types/d3-ease" "*" + "@types/d3-fetch" "*" + "@types/d3-force" "*" + "@types/d3-format" "*" + "@types/d3-geo" "*" + "@types/d3-hierarchy" "*" + "@types/d3-interpolate" "*" + "@types/d3-path" "*" + "@types/d3-polygon" "*" + "@types/d3-quadtree" "*" + "@types/d3-random" "*" + "@types/d3-scale" "*" + "@types/d3-scale-chromatic" "*" + "@types/d3-selection" "*" + "@types/d3-shape" "*" + "@types/d3-time" "*" + "@types/d3-time-format" "*" + "@types/d3-timer" "*" + "@types/d3-transition" "*" + "@types/d3-zoom" "*" + "@types/eslint-scope@^3.7.7": version "3.7.7" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" @@ -3384,6 +3583,11 @@ dependencies: "@types/jquery" "*" +"@types/trusted-types@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" + integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== + "@types/ws@^8.5.10": version "8.5.12" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e" @@ -3487,6 +3691,14 @@ "@typescript-eslint/types" "8.54.0" eslint-visitor-keys "^4.2.1" +"@upsetjs/venn.js@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@upsetjs/venn.js/-/venn.js-2.0.0.tgz#3be192038cdda927aa4f8b22ab51af82abf47f34" + integrity sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw== + optionalDependencies: + d3-selection "^3.0.0" + d3-transition "^3.0.1" + "@vitejs/plugin-basic-ssl@2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz#c70d2a922bc437f154089d7ef0505db4b383eb7b" @@ -3683,10 +3895,10 @@ acorn-walk@^8.1.1: dependencies: acorn "^8.11.0" -acorn@^8.11.0, acorn@^8.11.3, acorn@^8.14.0, acorn@^8.15.0, acorn@^8.4.1: - version "8.15.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" - integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== +acorn@^8.11.0, acorn@^8.14.0, acorn@^8.15.0, acorn@^8.16.0, acorn@^8.4.1: + version "8.16.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" + integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== adjust-sourcemap-loader@^4.0.0: version "4.0.0" @@ -4305,24 +4517,23 @@ chardet@^2.1.1: resolved "https://registry.yarnpkg.com/chardet/-/chardet-2.1.1.tgz#5c75593704a642f71ee53717df234031e65373c8" integrity sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ== -chevrotain-allstar@~0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz#b7412755f5d83cc139ab65810cdb00d8db40e6ca" - integrity sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw== +chevrotain-allstar@~0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/chevrotain-allstar/-/chevrotain-allstar-0.4.1.tgz#04e1429faca94a14d4572e0107c4865beac36298" + integrity sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA== dependencies: lodash-es "^4.17.21" -chevrotain@~11.0.3: - version "11.0.3" - resolved "https://registry.yarnpkg.com/chevrotain/-/chevrotain-11.0.3.tgz#88ffc1fb4b5739c715807eaeedbbf200e202fc1b" - integrity sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw== +chevrotain@~12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/chevrotain/-/chevrotain-12.0.0.tgz#8ebefe0a0516b1b314a8d9c7f4e948a509098d1c" + integrity sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ== dependencies: - "@chevrotain/cst-dts-gen" "11.0.3" - "@chevrotain/gast" "11.0.3" - "@chevrotain/regexp-to-ast" "11.0.3" - "@chevrotain/types" "11.0.3" - "@chevrotain/utils" "11.0.3" - lodash-es "4.17.21" + "@chevrotain/cst-dts-gen" "12.0.0" + "@chevrotain/gast" "12.0.0" + "@chevrotain/regexp-to-ast" "12.0.0" + "@chevrotain/types" "12.0.0" + "@chevrotain/utils" "12.0.0" chokidar@^3.6.0: version "3.6.0" @@ -4538,10 +4749,10 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -confbox@^0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.7.tgz#ccfc0a2bcae36a84838e83a3b7f770fb17d6c579" - integrity sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA== +confbox@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06" + integrity sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w== config-chain@^1.1.13: version "1.1.13" @@ -4746,10 +4957,10 @@ cytoscape-fcose@^2.2.0: dependencies: cose-base "^2.2.0" -cytoscape@^3.29.2: - version "3.30.2" - resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.30.2.tgz#94149707fb6547a55e3b44f03ffe232706212161" - integrity sha512-oICxQsjW8uSaRmn4UK/jkczKOqTrVqt5/1WL0POiJUT2EKNc9STM4hYFHv917yu55aTBMFNRzymlJhVAiWPCxw== +cytoscape@^3.33.1: + version "3.33.2" + resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.33.2.tgz#3a58906b4002b7c237f54dfc9b971983757da791" + integrity sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw== "d3-array@1 - 2": version "2.12.1" @@ -4926,7 +5137,7 @@ d3-scale@4: d3-time "2.1.1 - 3" d3-time-format "2 - 4" -"d3-selection@2 - 3", d3-selection@3: +"d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== @@ -4964,7 +5175,7 @@ d3-shape@^1.2.0: resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== -"d3-transition@2 - 3", d3-transition@3: +"d3-transition@2 - 3", d3-transition@3, d3-transition@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f" integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w== @@ -4986,7 +5197,7 @@ d3-zoom@3: d3-selection "2 - 3" d3-transition "2 - 3" -d3@^7.8.2, d3@^7.9.0: +d3@^7.9.0: version "7.9.0" resolved "https://registry.yarnpkg.com/d3/-/d3-7.9.0.tgz#579e7acb3d749caf8860bd1741ae8d371070cd5d" integrity sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA== @@ -5022,12 +5233,12 @@ d3@^7.8.2, d3@^7.9.0: d3-transition "3" d3-zoom "3" -dagre-d3-es@7.0.10: - version "7.0.10" - resolved "https://registry.yarnpkg.com/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz#19800d4be674379a3cd8c86a8216a2ac6827cadc" - integrity sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A== +dagre-d3-es@7.0.14: + version "7.0.14" + resolved "https://registry.yarnpkg.com/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz#1272276e26457cf3b97dac569f8f0531ec33c377" + integrity sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg== dependencies: - d3 "^7.8.2" + d3 "^7.9.0" lodash-es "^4.17.21" data-uri-to-buffer@^4.0.0: @@ -5062,11 +5273,16 @@ data-view-byte-offset@^1.0.1: es-errors "^1.3.0" is-data-view "^1.0.1" -dayjs@1.11.19, dayjs@^1.11.10, dayjs@^1.11.5: +dayjs@1.11.19: version "1.11.19" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.19.tgz#15dc98e854bb43917f12021806af897c58ae2938" integrity sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw== +dayjs@^1.11.19, dayjs@^1.11.5: + version "1.11.20" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.20.tgz#88d919fd639dc991415da5f4cb6f1b6650811938" + integrity sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ== + debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -5256,10 +5472,12 @@ domhandler@^5.0.2, domhandler@^5.0.3: dependencies: domelementtype "^2.3.0" -dompurify@^3.0.11: - version "3.1.7" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.7.tgz#711a8c96479fb6ced93453732c160c3c72418a6a" - integrity sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ== +dompurify@^3.3.1: + version "3.4.0" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.0.tgz#b1fc33ebdadb373241621e0a30e4ad81573dfd0b" + integrity sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg== + optionalDependencies: + "@types/trusted-types" "^2.0.7" domutils@^3.2.2: version "3.2.2" @@ -7206,10 +7424,10 @@ karma-source-map-support@1.4.0: dependencies: source-map-support "^0.5.5" -katex@^0.16.0, katex@^0.16.9: - version "0.16.11" - resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.11.tgz#4bc84d5584f996abece5f01c6ad11304276a33f5" - integrity sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ== +katex@^0.16.0, katex@^0.16.25: + version "0.16.45" + resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.45.tgz#ba60d39c54746b6b8d39ce0e7f6eace07143149c" + integrity sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA== dependencies: commander "^8.3.0" @@ -7242,21 +7460,17 @@ klaw-sync@^6.0.0: dependencies: graceful-fs "^4.1.11" -kolorist@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/kolorist/-/kolorist-1.8.0.tgz#edddbbbc7894bc13302cdf740af6374d4a04743c" - integrity sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ== - -langium@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/langium/-/langium-3.0.0.tgz#4938294eb57c59066ef955070ac4d0c917b26026" - integrity sha512-+Ez9EoiByeoTu/2BXmEaZ06iPNXM6thWJp02KfBO/raSMyCJ4jw7AkWWa+zBCTm0+Tw1Fj9FOxdqSskyN5nAwg== +langium@^4.0.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/langium/-/langium-4.2.2.tgz#d7409c23475d591ed6fc7d123c396e4fa4134e60" + integrity sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ== dependencies: - chevrotain "~11.0.3" - chevrotain-allstar "~0.3.0" + "@chevrotain/regexp-to-ast" "~12.0.0" + chevrotain "~12.0.0" + chevrotain-allstar "~0.4.1" vscode-languageserver "~9.0.1" vscode-languageserver-textdocument "~1.0.11" - vscode-uri "~3.0.8" + vscode-uri "~3.1.0" launch-editor@^2.6.1: version "2.9.1" @@ -7422,14 +7636,6 @@ loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" -local-pkg@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.5.0.tgz#093d25a346bae59a99f80e75f6e9d36d7e8c925c" - integrity sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg== - dependencies: - mlly "^1.4.2" - pkg-types "^1.0.3" - locate-path@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" @@ -7444,10 +7650,10 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash-es@4.17.21, lodash-es@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" - integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== +lodash-es@^4.17.21, lodash-es@^4.17.23: + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.18.1.tgz#b962eeb80d9d983a900bf342961fb7418ca10b1d" + integrity sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A== lodash.camelcase@^4.3.0: version "4.3.0" @@ -7584,12 +7790,7 @@ maplibre-gl@5.2.0: tinyqueue "^3.0.0" vt-pbf "^3.1.3" -marked@^13.0.2: - version "13.0.3" - resolved "https://registry.yarnpkg.com/marked/-/marked-13.0.3.tgz#5c5b4a5d0198060c7c9bc6ef9420a7fed30f822d" - integrity sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA== - -marked@~16.4.2: +marked@^16.3.0, marked@~16.4.2: version "16.4.2" resolved "https://registry.yarnpkg.com/marked/-/marked-16.4.2.tgz#4959a64be6c486f0db7467ead7ce288de54290a3" integrity sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA== @@ -7650,29 +7851,31 @@ merge2@^1.3.0: integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== "mermaid@>= 10.6.0 < 12.0.0": - version "11.2.1" - resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-11.2.1.tgz#b168c6f862268f77a0d3559926b193926ddc60bc" - integrity sha512-F8TEaLVVyxTUmvKswVFyOkjPrlJA5h5vNR1f7ZnSWSpqxgEZG1hggtn/QCa7znC28bhlcrNh10qYaIiill7q4A== - dependencies: - "@braintree/sanitize-url" "^7.0.1" - "@iconify/utils" "^2.1.32" - "@mermaid-js/parser" "^0.3.0" - cytoscape "^3.29.2" + version "11.14.0" + resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-11.14.0.tgz#ce81b22bc10f3117ef7737406ef2d10ee1741769" + integrity sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g== + dependencies: + "@braintree/sanitize-url" "^7.1.1" + "@iconify/utils" "^3.0.2" + "@mermaid-js/parser" "^1.1.0" + "@types/d3" "^7.4.3" + "@upsetjs/venn.js" "^2.0.0" + cytoscape "^3.33.1" cytoscape-cose-bilkent "^4.1.0" cytoscape-fcose "^2.2.0" d3 "^7.9.0" d3-sankey "^0.12.3" - dagre-d3-es "7.0.10" - dayjs "^1.11.10" - dompurify "^3.0.11" - katex "^0.16.9" + dagre-d3-es "7.0.14" + dayjs "^1.11.19" + dompurify "^3.3.1" + katex "^0.16.25" khroma "^2.1.0" - lodash-es "^4.17.21" - marked "^13.0.2" + lodash-es "^4.17.23" + marked "^16.3.0" roughjs "^4.6.6" - stylis "^4.3.1" + stylis "^4.3.6" ts-dedent "^2.2.0" - uuid "^9.0.1" + uuid "^11.1.0" methods@~1.1.2: version "1.1.2" @@ -7818,15 +8021,15 @@ minizlib@^3.0.1, minizlib@^3.1.0: dependencies: minipass "^7.1.2" -mlly@^1.4.2, mlly@^1.7.1: - version "1.7.1" - resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.1.tgz#e0336429bb0731b6a8e887b438cbdae522c8f32f" - integrity sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA== +mlly@^1.7.4, mlly@^1.8.0: + version "1.8.2" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.8.2.tgz#e7f7919a82d13b174405613117249a3f449d78bb" + integrity sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA== dependencies: - acorn "^8.11.3" - pathe "^1.1.2" - pkg-types "^1.1.1" - ufo "^1.5.3" + acorn "^8.16.0" + pathe "^2.0.3" + pkg-types "^1.3.1" + ufo "^1.6.3" moment-timezone@^0.6.0: version "0.6.0" @@ -8400,10 +8603,10 @@ package-json-from-dist@^1.0.0: resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== -package-manager-detector@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/package-manager-detector/-/package-manager-detector-0.2.0.tgz#160395cd5809181f5a047222319262b8c2d8aaea" - integrity sha512-E385OSk9qDcXhcM9LNSe4sdhx8a9mAPrZ4sMLW+tmxl5ZuGtPUcdFu+MPP2jbgiWAZ6Pfe5soGFMd+0Db5Vrog== +package-manager-detector@^1.3.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/package-manager-detector/-/package-manager-detector-1.6.0.tgz#70d0cf0aa02c877eeaf66c4d984ede0be9130734" + integrity sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA== pacote@21.0.4: version "21.0.4" @@ -8561,10 +8764,10 @@ path-to-regexp@~0.1.12: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.13.tgz#9b22ec16bc3ab88d05a0c7e369869421401ab17d" integrity sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA== -pathe@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" - integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== +pathe@^2.0.1, pathe@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== pbf@^3.2.1, pbf@^3.3.0: version "3.3.0" @@ -8616,14 +8819,14 @@ pkce-challenge@^5.0.0: resolved "https://registry.yarnpkg.com/pkce-challenge/-/pkce-challenge-5.0.1.tgz#3b4446865b17b1745e9ace2016a31f48ddf6230d" integrity sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ== -pkg-types@^1.0.3, pkg-types@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.2.0.tgz#d0268e894e93acff11a6279de147e83354ebd42d" - integrity sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA== +pkg-types@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.3.1.tgz#bd7cc70881192777eef5326c19deb46e890917df" + integrity sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ== dependencies: - confbox "^0.1.7" - mlly "^1.7.1" - pathe "^1.1.2" + confbox "^0.1.8" + mlly "^1.7.4" + pathe "^2.0.1" pngjs@^5.0.0: version "5.0.0" @@ -9832,10 +10035,10 @@ strip-json-comments@3.1.1, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -stylis@^4.3.1: - version "4.3.4" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.4.tgz#ca5c6c4a35c4784e4e93a2a24dc4e9fa075250a4" - integrity sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now== +stylis@^4.3.6: + version "4.4.0" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.4.0.tgz#c5846c9345f4bfc51bd0cbd7ca35a0744f485a5d" + integrity sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA== sucrase@^3.35.0: version "3.35.1" @@ -10013,10 +10216,10 @@ tinycolor2@^1.6.0: resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e" integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw== -tinyexec@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.0.tgz#ed60cfce19c17799d4a241e06b31b0ec2bee69e6" - integrity sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg== +tinyexec@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.1.1.tgz#e1ff45dfa60d1dedb91b734956b78f6c2a3e821b" + integrity sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg== tinyglobby@0.2.14: version "0.2.14" @@ -10260,10 +10463,10 @@ typical@^5.2.0: resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066" integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg== -ufo@^1.5.3: - version "1.5.4" - resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754" - integrity sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ== +ufo@^1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.3.tgz#799666e4e88c122a9659805e30b9dc071c3aed4f" + integrity sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q== unbox-primitive@^1.1.0: version "1.1.0" @@ -10359,16 +10562,16 @@ utrie@^1.0.2: dependencies: base64-arraybuffer "^1.0.2" +uuid@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912" + integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A== + uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@^9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" - integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== - v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" @@ -10441,10 +10644,10 @@ vscode-languageserver@~9.0.1: dependencies: vscode-languageserver-protocol "3.17.5" -vscode-uri@~3.0.8: - version "3.0.8" - resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f" - integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw== +vscode-uri@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.1.0.tgz#dd09ec5a66a38b5c3fffc774015713496d14e09c" + integrity sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ== vt-pbf@^3.1.3: version "3.1.3" From 1549dc9cc16b3cdd34bf1be2535c8516e191ce35 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 21 Apr 2026 11:28:45 +0300 Subject: [PATCH 18/25] Fixed CVE-2026-4800 --- ui-ngx/package.json | 2 +- .../widget/lib/maps-legacy/leaflet-map.ts | 4 +- ui-ngx/yarn.lock | 365 +++++++++--------- 3 files changed, 180 insertions(+), 191 deletions(-) diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 4f34216e6c..7952d34db8 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -26,7 +26,7 @@ "@auth0/angular-jwt": "^5.2.0", "@flowjs/flow.js": "^2.14.1", "@flowjs/ngx-flow": "20.0.2", - "@geoman-io/leaflet-geoman-free": "2.18.3", + "@geoman-io/leaflet-geoman-free": "2.19.3", "@iplab/ngx-color-picker": "^20.0.0", "@mat-datetimepicker/core": "~16.0.1", "@mdi/svg": "^7.4.47", diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/leaflet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/leaflet-map.ts index 0b00c5970e..ba40af770c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/leaflet-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps-legacy/leaflet-map.ts @@ -289,7 +289,7 @@ export default abstract class LeafletMap { } private toggleDrawMode(type: string) { - this.map.pm.Draw[type].toggle(); + (this.map.pm.Draw[type] as any).toggle(); } addEditControl() { @@ -373,7 +373,7 @@ export default abstract class LeafletMap { }, // @ts-ignore afterClick: (e, ctx) => { - this.map.pm.Draw[ctx.button._button.jsClass].toggle({ + (this.map.pm.Draw[ctx.button._button.jsClass] as any).toggle({ snappable: this.options.snappable, cursorMarker: true, allowSelfIntersection: false, diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 454822d186..319cf7eaf1 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -1592,17 +1592,17 @@ dependencies: tslib "^2.3.0" -"@geoman-io/leaflet-geoman-free@2.18.3": - version "2.18.3" - resolved "https://registry.yarnpkg.com/@geoman-io/leaflet-geoman-free/-/leaflet-geoman-free-2.18.3.tgz#a41489920b175931fba2a1e8e81347f9e3be5481" - integrity sha512-XzxSKRk2UJUVeGiOt1jU2hyo412Qee1Q0Xsfw4A2r8EoUIo48XKSWfusYe7E53fSPr0aYgZxPevnFdcUXimpdA== - dependencies: - "@turf/boolean-contains" "^6.5.0" - "@turf/kinks" "^6.5.0" - "@turf/line-intersect" "^6.5.0" - "@turf/line-split" "^6.5.0" - lodash "4.17.21" - polyclip-ts "^0.16.5" +"@geoman-io/leaflet-geoman-free@2.19.3": + version "2.19.3" + resolved "https://registry.yarnpkg.com/@geoman-io/leaflet-geoman-free/-/leaflet-geoman-free-2.19.3.tgz#42ea4d10be3a76c64376cdd8736b191dd0c5ae2d" + integrity sha512-HjbEpfAEUs0NyI1Dhvz3SMVG6m0pAN/1Eo0tRKsz9cpaROTrFtmJGY22swEir1Uj/8IeGF1NJId38C5Fu+nZGQ== + dependencies: + "@turf/boolean-contains" "^7.3.3" + "@turf/kinks" "^7.3.3" + "@turf/line-intersect" "^7.3.3" + "@turf/line-split" "^7.3.3" + lodash "4.18.1" + polyclip-ts "^0.16.8" "@hono/node-server@^1.19.9": version "1.19.11" @@ -2860,181 +2860,167 @@ "@tufjs/canonical-json" "2.0.0" minimatch "^10.1.1" -"@turf/bbox@*": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@turf/bbox/-/bbox-7.1.0.tgz#45a9287c084f7b79577ee88b7b539d83562b923b" - integrity sha512-PdWPz9tW86PD78vSZj2fiRaB8JhUHy6piSa/QXb83lucxPK+HTAdzlDQMTKj5okRCU8Ox/25IR2ep9T8NdopRA== +"@turf/bbox@7.3.5": + version "7.3.5" + resolved "https://registry.yarnpkg.com/@turf/bbox/-/bbox-7.3.5.tgz#bf93e0a81aa98ca8809bea1bfa6cdfd03ce2470a" + integrity sha512-oG1ya/HtBjAIg4TimbWx+nOYPbY0bCvt82Bq8tm6sBw3qqtbOyRSfDz79Sq90TnH7DXJprJ1qnVGKNtZ6jemfw== dependencies: - "@turf/helpers" "^7.1.0" - "@turf/meta" "^7.1.0" + "@turf/helpers" "7.3.5" + "@turf/meta" "7.3.5" "@types/geojson" "^7946.0.10" - tslib "^2.6.2" - -"@turf/bbox@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/bbox/-/bbox-6.5.0.tgz#bec30a744019eae420dac9ea46fb75caa44d8dc5" - integrity sha512-RBbLaao5hXTYyyg577iuMtDB8ehxMlUqHEJiMs8jT1GHkFhr6sYre3lmLsPeYEi/ZKj5TP5tt7fkzNdJ4GIVyw== - dependencies: - "@turf/helpers" "^6.5.0" - "@turf/meta" "^6.5.0" - -"@turf/bearing@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/bearing/-/bearing-6.5.0.tgz#462a053c6c644434bdb636b39f8f43fb0cd857b0" - integrity sha512-dxINYhIEMzgDOztyMZc20I7ssYVNEpSv04VbMo5YPQsqa80KO3TFvbuCahMsCAW5z8Tncc8dwBlEFrmRjJG33A== - dependencies: - "@turf/helpers" "^6.5.0" - "@turf/invariant" "^6.5.0" - -"@turf/boolean-contains@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/boolean-contains/-/boolean-contains-6.5.0.tgz#f802e7432fb53109242d5bf57393ef2f53849bbf" - integrity sha512-4m8cJpbw+YQcKVGi8y0cHhBUnYT+QRfx6wzM4GI1IdtYH3p4oh/DOBJKrepQyiDzFDaNIjxuWXBh0ai1zVwOQQ== - dependencies: - "@turf/bbox" "^6.5.0" - "@turf/boolean-point-in-polygon" "^6.5.0" - "@turf/boolean-point-on-line" "^6.5.0" - "@turf/helpers" "^6.5.0" - "@turf/invariant" "^6.5.0" - -"@turf/boolean-point-in-polygon@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-6.5.0.tgz#6d2e9c89de4cd2e4365004c1e51490b7795a63cf" - integrity sha512-DtSuVFB26SI+hj0SjrvXowGTUCHlgevPAIsukssW6BG5MlNSBQAo70wpICBNJL6RjukXg8d2eXaAWuD/CqL00A== - dependencies: - "@turf/helpers" "^6.5.0" - "@turf/invariant" "^6.5.0" + tslib "^2.8.1" + +"@turf/boolean-contains@^7.3.3": + version "7.3.5" + resolved "https://registry.yarnpkg.com/@turf/boolean-contains/-/boolean-contains-7.3.5.tgz#8f3d3bd996b98a23953c42a5db932f17bcb98b68" + integrity sha512-P4JUAHgvJkD+8ybQ6d1OHp9TBsGsjJxF5lWeXJgp0k4+Hd/D0CVy4/mhLkZdNa6QdljVdwNcfU0CTqy1WsSQig== + dependencies: + "@turf/bbox" "7.3.5" + "@turf/boolean-point-in-polygon" "7.3.5" + "@turf/boolean-point-on-line" "7.3.5" + "@turf/helpers" "7.3.5" + "@turf/invariant" "7.3.5" + "@turf/line-split" "7.3.5" + "@types/geojson" "^7946.0.10" + tslib "^2.8.1" -"@turf/boolean-point-on-line@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/boolean-point-on-line/-/boolean-point-on-line-6.5.0.tgz#a8efa7bad88760676f395afb9980746bc5b376e9" - integrity sha512-A1BbuQ0LceLHvq7F/P7w3QvfpmZqbmViIUPHdNLvZimFNLo4e6IQunmzbe+8aSStH9QRZm3VOflyvNeXvvpZEQ== +"@turf/boolean-point-in-polygon@7.3.5": + version "7.3.5" + resolved "https://registry.yarnpkg.com/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-7.3.5.tgz#4416dbde721225153590cc2204f614e44f388b55" + integrity sha512-ba7+B0wzaS9GtERZOoXUZ6oW8IcIJHNQZf3c+tiD9ESjcsPO1Q/4qIJGTKl92nBLhhracHJxMWBM/U6hAVkaRg== dependencies: - "@turf/helpers" "^6.5.0" - "@turf/invariant" "^6.5.0" + "@turf/helpers" "7.3.5" + "@turf/invariant" "7.3.5" + "@types/geojson" "^7946.0.10" + point-in-polygon-hao "^1.1.0" + tslib "^2.8.1" -"@turf/destination@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/destination/-/destination-6.5.0.tgz#30a84702f9677d076130e0440d3223ae503fdae1" - integrity sha512-4cnWQlNC8d1tItOz9B4pmJdWpXqS0vEvv65bI/Pj/genJnsL7evI0/Xw42RvEGROS481MPiU80xzvwxEvhQiMQ== +"@turf/boolean-point-on-line@7.3.5": + version "7.3.5" + resolved "https://registry.yarnpkg.com/@turf/boolean-point-on-line/-/boolean-point-on-line-7.3.5.tgz#9ae059b836240c5e4619c5632682db8acbe511b6" + integrity sha512-TuWfrAT63W43BDzgYc94UzQ5/PjF1aTnh4AIzmQwez1YnimShYcOTwo8OIHzDaB6gbbvFsfxYMuOA5JOp942Kg== dependencies: - "@turf/helpers" "^6.5.0" - "@turf/invariant" "^6.5.0" + "@turf/helpers" "7.3.5" + "@turf/invariant" "7.3.5" + "@types/geojson" "^7946.0.10" + tslib "^2.8.1" -"@turf/distance@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/distance/-/distance-6.5.0.tgz#21f04d5f86e864d54e2abde16f35c15b4f36149a" - integrity sha512-xzykSLfoURec5qvQJcfifw/1mJa+5UwByZZ5TZ8iaqjGYN0vomhV9aiSLeYdUGtYRESZ+DYC/OzY+4RclZYgMg== +"@turf/distance@7.3.5": + version "7.3.5" + resolved "https://registry.yarnpkg.com/@turf/distance/-/distance-7.3.5.tgz#3aa16a1fde30e5cf4cf40b7e497be8cc5d6bbaaf" + integrity sha512-uQAC63zg/l91KUxzfhqio7Ii3+UXTrPOVJScIdRj6EO6+9XHI4kC+AdyIS4cPAv14sZfJLIBxzMnzcGrss+kEA== dependencies: - "@turf/helpers" "^6.5.0" - "@turf/invariant" "^6.5.0" - -"@turf/helpers@6.x", "@turf/helpers@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/helpers/-/helpers-6.5.0.tgz#f79af094bd6b8ce7ed2bd3e089a8493ee6cae82e" - integrity sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw== + "@turf/helpers" "7.3.5" + "@turf/invariant" "7.3.5" + "@types/geojson" "^7946.0.10" + tslib "^2.8.1" -"@turf/helpers@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@turf/helpers/-/helpers-7.1.0.tgz#eb734e291c9c205822acdd289fe20e91c3cb1641" - integrity sha512-dTeILEUVeNbaEeoZUOhxH5auv7WWlOShbx7QSd4s0T4Z0/iz90z9yaVCtZOLbU89umKotwKaJQltBNO9CzVgaQ== +"@turf/geojson-rbush@7.3.5": + version "7.3.5" + resolved "https://registry.yarnpkg.com/@turf/geojson-rbush/-/geojson-rbush-7.3.5.tgz#32e38b0b85b4dcf9af5aa433ddbb2e2702048e0a" + integrity sha512-30/hQqc+ErnlcavvDdxGfgm8VtsJDEzSYpf3mPqYxOyI976l49T6+1jCQD5xKswml6o8zZAaTSe6ZcSKF+SCNw== dependencies: + "@turf/bbox" "7.3.5" + "@turf/helpers" "7.3.5" + "@turf/meta" "7.3.5" "@types/geojson" "^7946.0.10" - tslib "^2.6.2" + rbush "^3.0.1" + tslib "^2.8.1" -"@turf/invariant@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/invariant/-/invariant-6.5.0.tgz#970afc988023e39c7ccab2341bd06979ddc7463f" - integrity sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg== +"@turf/helpers@7.3.5": + version "7.3.5" + resolved "https://registry.yarnpkg.com/@turf/helpers/-/helpers-7.3.5.tgz#051928c03cdf9ffcc7ae36581c317afc49bfd999" + integrity sha512-E/NMGV5MwbjjP7AJXBtsanC3yY8N2MQ87IGdIgkB2ji5AtBpwnH4L3gEqpYN4RlCJJWbLbzO91BbKv2waUd0eg== dependencies: - "@turf/helpers" "^6.5.0" + "@types/geojson" "^7946.0.10" + tslib "^2.8.1" -"@turf/kinks@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/kinks/-/kinks-6.5.0.tgz#80e7456367535365012f658cf1a988b39a2c920b" - integrity sha512-ViCngdPt1eEL7hYUHR2eHR662GvCgTc35ZJFaNR6kRtr6D8plLaDju0FILeFFWSc+o8e3fwxZEJKmFj9IzPiIQ== +"@turf/invariant@7.3.5": + version "7.3.5" + resolved "https://registry.yarnpkg.com/@turf/invariant/-/invariant-7.3.5.tgz#5619d0e0ef3755e2be69855bad47e10158a1820b" + integrity sha512-ZVIvsBvjr8lO7WxC5zYNjRsjSDvyGvWkJMjuWaJjTU8x+1tmfNnw3gDX/TI2Sit83gcRYLYkNo23lB/udqx/Hg== dependencies: - "@turf/helpers" "^6.5.0" + "@turf/helpers" "7.3.5" + "@types/geojson" "^7946.0.10" + tslib "^2.8.1" -"@turf/line-intersect@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/line-intersect/-/line-intersect-6.5.0.tgz#dea48348b30c093715d2195d2dd7524aee4cf020" - integrity sha512-CS6R1tZvVQD390G9Ea4pmpM6mJGPWoL82jD46y0q1KSor9s6HupMIo1kY4Ny+AEYQl9jd21V3Scz20eldpbTVA== +"@turf/kinks@^7.3.3": + version "7.3.5" + resolved "https://registry.yarnpkg.com/@turf/kinks/-/kinks-7.3.5.tgz#746c2295dd8e7ddc8822d13fc6b9ae8d4148babc" + integrity sha512-dPW8d4vs1v8WMobjyq/TVqajjPwkMsl94IF58yp1UYlmJDQrW4iNRUmI9fFzww+fl7epCKNwY+jZhXf1DRi93w== dependencies: - "@turf/helpers" "^6.5.0" - "@turf/invariant" "^6.5.0" - "@turf/line-segment" "^6.5.0" - "@turf/meta" "^6.5.0" - geojson-rbush "3.x" + "@turf/helpers" "7.3.5" + "@types/geojson" "^7946.0.10" + tslib "^2.8.1" -"@turf/line-segment@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/line-segment/-/line-segment-6.5.0.tgz#ee73f3ffcb7c956203b64ed966d96af380a4dd65" - integrity sha512-jI625Ho4jSuJESNq66Mmi290ZJ5pPZiQZruPVpmHkUw257Pew0alMmb6YrqYNnLUuiVVONxAAKXUVeeUGtycfw== +"@turf/line-intersect@7.3.5", "@turf/line-intersect@^7.3.3": + version "7.3.5" + resolved "https://registry.yarnpkg.com/@turf/line-intersect/-/line-intersect-7.3.5.tgz#841f8f713218cbd4532269754e3e1f6a15d4e374" + integrity sha512-2Cl4oPsjaDdfIwz/5IRDdG2fNdfp3W6atICm81vnzl/GwURoVP+CLjXJ64QWWzpzIbgX2XprJQTmamByDt5MDw== dependencies: - "@turf/helpers" "^6.5.0" - "@turf/invariant" "^6.5.0" - "@turf/meta" "^6.5.0" + "@turf/helpers" "7.3.5" + "@types/geojson" "^7946.0.10" + sweepline-intersections "^1.5.0" + tslib "^2.8.1" -"@turf/line-split@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/line-split/-/line-split-6.5.0.tgz#116d7fbf714457878225187f5820ef98db7b02c2" - integrity sha512-/rwUMVr9OI2ccJjw7/6eTN53URtGThNSD5I0GgxyFXMtxWiloRJ9MTff8jBbtPWrRka/Sh2GkwucVRAEakx9Sw== - dependencies: - "@turf/bbox" "^6.5.0" - "@turf/helpers" "^6.5.0" - "@turf/invariant" "^6.5.0" - "@turf/line-intersect" "^6.5.0" - "@turf/line-segment" "^6.5.0" - "@turf/meta" "^6.5.0" - "@turf/nearest-point-on-line" "^6.5.0" - "@turf/square" "^6.5.0" - "@turf/truncate" "^6.5.0" - geojson-rbush "3.x" - -"@turf/meta@6.x", "@turf/meta@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/meta/-/meta-6.5.0.tgz#b725c3653c9f432133eaa04d3421f7e51e0418ca" - integrity sha512-RrArvtsV0vdsCBegoBtOalgdSOfkBrTJ07VkpiCnq/491W67hnMWmDu7e6Ztw0C3WldRYTXkg3SumfdzZxLBHA== +"@turf/line-segment@7.3.5": + version "7.3.5" + resolved "https://registry.yarnpkg.com/@turf/line-segment/-/line-segment-7.3.5.tgz#275f03fd86640fce44b40d6806b95dc842801156" + integrity sha512-TM1aCu7utM6fllAEHO8PNqBJZ/uoFJVNp2A0YI7FyWN928hPbacsvNtLeVz/Kq1ZbeqQ1ZIKRxo9FdVjaj8hGg== dependencies: - "@turf/helpers" "^6.5.0" + "@turf/helpers" "7.3.5" + "@turf/invariant" "7.3.5" + "@turf/meta" "7.3.5" + "@types/geojson" "^7946.0.10" + tslib "^2.8.1" + +"@turf/line-split@7.3.5", "@turf/line-split@^7.3.3": + version "7.3.5" + resolved "https://registry.yarnpkg.com/@turf/line-split/-/line-split-7.3.5.tgz#63aa7c5d3fc606db7c4beff33dccc8786fc9c07b" + integrity sha512-GEuy+LdbbaqtYjHk/i1G8sK51wfCdPqTO8uH0dJZ6WlcIcZQfRcKKI4ksFm7NkVyfmw8gXWbpMJD8lO380GFBQ== + dependencies: + "@turf/bbox" "7.3.5" + "@turf/geojson-rbush" "7.3.5" + "@turf/helpers" "7.3.5" + "@turf/invariant" "7.3.5" + "@turf/line-intersect" "7.3.5" + "@turf/line-segment" "7.3.5" + "@turf/meta" "7.3.5" + "@turf/nearest-point-on-line" "7.3.5" + "@turf/truncate" "7.3.5" + "@types/geojson" "^7946.0.10" + tslib "^2.8.1" -"@turf/meta@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@turf/meta/-/meta-7.1.0.tgz#b2af85afddd0ef08aeae8694a12370a4f06b6d13" - integrity sha512-ZgGpWWiKz797Fe8lfRj7HKCkGR+nSJ/5aKXMyofCvLSc2PuYJs/qyyifDPWjASQQCzseJ7AlF2Pc/XQ/3XkkuA== +"@turf/meta@7.3.5": + version "7.3.5" + resolved "https://registry.yarnpkg.com/@turf/meta/-/meta-7.3.5.tgz#c498a6f1e8603e014e8bfbc9a4b2760b6219edb9" + integrity sha512-r+ohqxoyqeigFB0oFrQx/YEHIkOKqcKpCjvZkvZs7Tkv+IFco5MezAd2zd4rzK+0DfFgDP3KpJc7HqrYjvEjhg== dependencies: - "@turf/helpers" "^7.1.0" + "@turf/helpers" "7.3.5" "@types/geojson" "^7946.0.10" + tslib "^2.8.1" -"@turf/nearest-point-on-line@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/nearest-point-on-line/-/nearest-point-on-line-6.5.0.tgz#8e1cd2cdc0b5acaf4c8d8b3b33bb008d3cb99e7b" - integrity sha512-WthrvddddvmymnC+Vf7BrkHGbDOUu6Z3/6bFYUGv1kxw8tiZ6n83/VG6kHz4poHOfS0RaNflzXSkmCi64fLBlg== - dependencies: - "@turf/bearing" "^6.5.0" - "@turf/destination" "^6.5.0" - "@turf/distance" "^6.5.0" - "@turf/helpers" "^6.5.0" - "@turf/invariant" "^6.5.0" - "@turf/line-intersect" "^6.5.0" - "@turf/meta" "^6.5.0" - -"@turf/square@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/square/-/square-6.5.0.tgz#ab43eef99d39c36157ab5b80416bbeba1f6b2122" - integrity sha512-BM2UyWDmiuHCadVhHXKIx5CQQbNCpOxB6S/aCNOCLbhCeypKX5Q0Aosc5YcmCJgkwO5BERCC6Ee7NMbNB2vHmQ== +"@turf/nearest-point-on-line@7.3.5": + version "7.3.5" + resolved "https://registry.yarnpkg.com/@turf/nearest-point-on-line/-/nearest-point-on-line-7.3.5.tgz#e74515e017feff2c8d7c208b634a5cbfdbebe998" + integrity sha512-MZn6OkEFZpjS6BNUANfqiHMIbQSivu7TNji3a+OAIrnPJ71vp8cbz0N2aVEa5M7I8ipvxoxAPIV3eqg3h280Vg== dependencies: - "@turf/distance" "^6.5.0" - "@turf/helpers" "^6.5.0" + "@turf/distance" "7.3.5" + "@turf/helpers" "7.3.5" + "@turf/invariant" "7.3.5" + "@turf/meta" "7.3.5" + "@types/geojson" "^7946.0.10" + tslib "^2.8.1" -"@turf/truncate@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/truncate/-/truncate-6.5.0.tgz#c3a16cad959f1be1c5156157d5555c64b19185d8" - integrity sha512-pFxg71pLk+eJj134Z9yUoRhIi8vqnnKvCYwdT4x/DQl/19RVdq1tV3yqOT3gcTQNfniteylL5qV1uTBDV5sgrg== +"@turf/truncate@7.3.5": + version "7.3.5" + resolved "https://registry.yarnpkg.com/@turf/truncate/-/truncate-7.3.5.tgz#d57a856994e2928e798fc7929e42cc2890d1b201" + integrity sha512-Qx2iv3KIqKuDAUduMfaJ5fFegEWBeRve5zePalRevS16bMUqEX+jnKPK9fWGyUuPqT61qP1Kybz0PTWPbUbljQ== dependencies: - "@turf/helpers" "^6.5.0" - "@turf/meta" "^6.5.0" + "@turf/helpers" "7.3.5" + "@turf/meta" "7.3.5" + "@types/geojson" "^7946.0.10" + tslib "^2.8.1" "@types/ace-diff@^2.1.4": version "2.1.4" @@ -3376,11 +3362,6 @@ resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.16.tgz#8ebe53d69efada7044454e3305c19017d97ced2a" integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg== -"@types/geojson@7946.0.8": - version "7946.0.8" - resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.8.tgz#30744afdb385e2945e22f3b033f897f76b1f12ca" - integrity sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA== - "@types/hammerjs@^2.0.45": version "2.0.45" resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.45.tgz#ffa764bb68a66c08db6efb9c816eb7be850577b1" @@ -6384,17 +6365,6 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -geojson-rbush@3.x: - version "3.2.0" - resolved "https://registry.yarnpkg.com/geojson-rbush/-/geojson-rbush-3.2.0.tgz#8b543cf0d56f99b78faf1da52bb66acad6dfc290" - integrity sha512-oVltQTXolxvsz1sZnutlSuLDEcQAKYC/uXt9zDzJJ6bu0W+baTI8LZBaTup5afzibEH4N3jlq2p+a152wlBJ7w== - dependencies: - "@turf/bbox" "*" - "@turf/helpers" "6.x" - "@turf/meta" "6.x" - "@types/geojson" "7946.0.8" - rbush "^3.0.1" - geojson-vt@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/geojson-vt/-/geojson-vt-4.0.2.tgz#1162f6c7d61a0ba305b1030621e6e111f847828a" @@ -7670,10 +7640,10 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@4.17.21, lodash@^4.17.14: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +lodash@4.18.1, lodash@^4.17.14: + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== log-driver@1.2.7: version "1.2.7" @@ -8833,6 +8803,13 @@ pngjs@^5.0.0: resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== +point-in-polygon-hao@^1.1.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/point-in-polygon-hao/-/point-in-polygon-hao-1.2.4.tgz#8662abdcc84bcca230cc3ecbb0b0ab1a306f1bd6" + integrity sha512-x2pcvXeqhRHlNRdhLs/tgFapAbSSe86wa/eqmj1G6pWftbEs5aVRJhRGM6FYSUERKu0PjekJzMq0gsI2XyiclQ== + dependencies: + robust-predicates "^3.0.2" + points-on-curve@0.2.0, points-on-curve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/points-on-curve/-/points-on-curve-0.2.0.tgz#7dbb98c43791859434284761330fa893cb81b4d1" @@ -8846,13 +8823,13 @@ points-on-path@^0.2.1: path-data-parser "0.1.0" points-on-curve "0.2.0" -polyclip-ts@^0.16.5: - version "0.16.5" - resolved "https://registry.yarnpkg.com/polyclip-ts/-/polyclip-ts-0.16.5.tgz#053e073e640449f1b1a1d88471f8758779d0b030" - integrity sha512-ZchnG0zGZReHgEo3EYzEUi6UmfQFFzNnj6AFU+gBm+IJJ4qG9gL4CwjtCV6oi/PittUPpJLiLJxcn/AgrCBO+g== +polyclip-ts@^0.16.8: + version "0.16.8" + resolved "https://registry.yarnpkg.com/polyclip-ts/-/polyclip-ts-0.16.8.tgz#503160d05e9d56380533aab0bc2dae835d6da5f9" + integrity sha512-JPtKbDRuPEuAjuTdhR62Gph7Is2BS1Szx69CFOO3g71lpJDFo78k4tFyi+qFOMVPePEzdSKkpGU3NBXPHHjvKQ== dependencies: bignumber.js "^9.1.0" - splaytree-ts "^1.0.1" + splaytree-ts "^1.0.2" possible-typed-array-names@^1.0.0: version "1.0.0" @@ -9877,10 +9854,10 @@ spdy@^4.0.2: select-hose "^2.0.0" spdy-transport "^3.0.0" -splaytree-ts@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/splaytree-ts/-/splaytree-ts-1.0.1.tgz#4ddcfe2684da017d02b599d53d67f6d07a90745b" - integrity sha512-B+VzCm33/KEchi/fzT6/3NRHm8k5+Kf37SBQO3meHHS/tK2xBnIm4ZvusQ1wUpHgKMCCqEWgXnwFXAa1nD289g== +splaytree-ts@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/splaytree-ts/-/splaytree-ts-1.0.2.tgz#34963704587aff45eaa09c24713f552bbf56e8f0" + integrity sha512-0kGecIZNIReCSiznK3uheYB8sbstLjCZLiwcQwbmLhgHJj2gz6OnSPkVzJQCMnmEz1BQ4gPK59ylhBoEWOhGNA== split.js@^1.6.5: version "1.6.5" @@ -10086,6 +10063,13 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +sweepline-intersections@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/sweepline-intersections/-/sweepline-intersections-1.5.0.tgz#85ab3629a291875926fae0acd508496430d8a647" + integrity sha512-AoVmx72QHpKtItPu72TzFL+kcYjd67BPLDoR0LarIk+xyaRg+pDTMFXndIEvZf9xEKnJv6JdhgRMnocoG0D3AQ== + dependencies: + tinyqueue "^2.0.0" + systemjs@6.15.1: version "6.15.1" resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-6.15.1.tgz#74175b6810e27a79e1177d21db5f0e3057118cea" @@ -10242,6 +10226,11 @@ tinymce@6.8.6, "tinymce@^7.0.0 || ^6.0.0 || ^5.5.0", tinymce@~6.8.6: resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-6.8.6.tgz#799e4f03eeb4399399dfdeb12ba17b3b91887adf" integrity sha512-++XYEs8lKWvZxDCjrr8Baiw7KiikraZ5JkLMg6EdnUVNKJui0IsrAADj5MsyUeFkcEryfn2jd3p09H7REvewyg== +tinyqueue@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08" + integrity sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA== + tinyqueue@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-3.0.0.tgz#101ea761ccc81f979e29200929e78f1556e3661e" @@ -10345,7 +10334,7 @@ tslib@2.3.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== -tslib@2.8.1, tslib@^2.0.0, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.6.2, tslib@^2.8.1, tslib@~2.8.1: +tslib@2.8.1, tslib@^2.0.0, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.8.1, tslib@~2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== From fe7d860c21393cf132b05f3452e4b8ab68eb6ad4 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 21 Apr 2026 11:43:40 +0300 Subject: [PATCH 19/25] Fixed dependencies --- ui-ngx/package.json | 2 +- ui-ngx/yarn.lock | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 7952d34db8..076e2d316a 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -45,7 +45,7 @@ "angular2-hotkeys": "^16.0.1", "canvas-gauges": "^2.1.7", "core-js": "^3.48.0", - "dayjs": "1.11.19", + "dayjs": "1.11.20", "echarts": "https://github.com/thingsboard/echarts/archive/5.5.2-TB.tar.gz", "flot": "https://github.com/thingsboard/flot.git#0.9-work", "flot.curvedlines": "https://github.com/MichaelZinsmaier/CurvedLines.git#master", diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 319cf7eaf1..4da832c885 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -5254,12 +5254,7 @@ data-view-byte-offset@^1.0.1: es-errors "^1.3.0" is-data-view "^1.0.1" -dayjs@1.11.19: - version "1.11.19" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.19.tgz#15dc98e854bb43917f12021806af897c58ae2938" - integrity sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw== - -dayjs@^1.11.19, dayjs@^1.11.5: +dayjs@1.11.20, dayjs@^1.11.19, dayjs@^1.11.5: version "1.11.20" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.20.tgz#88d919fd639dc991415da5f4cb6f1b6650811938" integrity sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ== From ba8d9af3629c8c937c051081773f78a9bac39a52 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 24 Apr 2026 16:06:23 +0300 Subject: [PATCH 20/25] Improve SSL reload test assertions --- .../transport/mqtt/MqttSslHandlerProviderTest.java | 7 +++++-- .../service/CertificateReloadManagerTest.java | 11 ++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProviderTest.java b/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProviderTest.java index 8c4f3c7a29..9b4ce007f8 100644 --- a/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProviderTest.java +++ b/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProviderTest.java @@ -33,6 +33,8 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -119,13 +121,13 @@ public class MqttSslHandlerProviderTest { CountDownLatch startLatch = new CountDownLatch(1); CountDownLatch doneLatch = new CountDownLatch(5); + List handlers = new CopyOnWriteArrayList<>(); for (int i = 0; i < 5; i++) { new Thread(() -> { try { startLatch.await(); - SslHandler handler = sslHandlerProvider.getSslHandler(); - assertThat(handler).isNotNull(); + handlers.add(sslHandlerProvider.getSslHandler()); } catch (Exception e) { throw new RuntimeException(e); } finally { @@ -138,6 +140,7 @@ public class MqttSslHandlerProviderTest { boolean completed = doneLatch.await(5, TimeUnit.SECONDS); assertThat(completed).isTrue(); + assertThat(handlers).hasSize(5).allSatisfy(h -> assertThat(h).isNotNull()); // Concurrent handshakes read the same pre-built context without the old sync bottleneck. SSLContext contextAfter = (SSLContext) ReflectionTestUtils.getField(sslHandlerProvider, "sslContext"); assertThat(contextAfter).isSameAs(contextBefore); diff --git a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java index 3156897210..6f78eaefae 100644 --- a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java +++ b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java @@ -24,7 +24,10 @@ import org.springframework.test.util.ReflectionTestUtils; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.attribute.FileTime; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -99,6 +102,9 @@ public class CertificateReloadManagerTest { certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); + long bumpedMtime = Files.getLastModifiedTime(certFile).toMillis() + 5_000L; + Files.setLastModifiedTime(certFile, FileTime.fromMillis(bumpedMtime)); + ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); assertThat(reloadCount.get()).isEqualTo(0); @@ -120,10 +126,13 @@ public class CertificateReloadManagerTest { @Test public void givenWatcherRegistered_whenShutdown_thenShouldStopScheduler() throws Exception { certificateReloadManager.registerWatcher("test-cert", certFile, () -> {}); + ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + ReflectionTestUtils.setField(certificateReloadManager, "scheduler", scheduler); certificateReloadManager.destroy(); - assertThat(certificateReloadManager).isNotNull(); + assertThat(scheduler.isShutdown()).isTrue(); + assertThat(scheduler.isTerminated()).isTrue(); } @Test From a75c71eeb6d9de454af6758ddb9ad6b0159f1dd0 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 27 Apr 2026 15:00:56 +0300 Subject: [PATCH 21/25] added spring compression properties --- application/src/main/resources/thingsboard.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index e1195b6d3b..43a2a23207 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -58,6 +58,14 @@ server: http2: # Enable/disable HTTP/2 support enabled: "${HTTP2_ENABLED:true}" + # HTTP response compression + compression: + # Enable/disable HTTP response compression + enabled: "${SERVER_COMPRESSION_ENABLED:false}" + # Minimum size (in bytes) required for a response before compression is applied + min-response-size: "${SERVER_COMPRESSION_MIN_RESPONSE_SIZE:2048}" + # Comma-separated list of MIME types that should be compressed + mime-types: "${SERVER_COMPRESSION_MIME_TYPES:text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json,application/xml}" # Log errors with stacktrace when REST API throws an exception with the message "Please contact sysadmin" log_controller_error_stack_trace: "${HTTP_LOG_CONTROLLER_ERROR_STACK_TRACE:false}" ws: From 5608fd2284c752ab5590b4c80c72bcc200441a28 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Fri, 24 Apr 2026 16:34:20 +0200 Subject: [PATCH 22/25] Fix map shape labels drifting from center after viewport resize --- .../widget/lib/maps/data-layer/circles-data-layer.ts | 3 ++- .../widget/lib/maps/data-layer/polygons-data-layer.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts index bec45b32e0..dcaf63c2ff 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts @@ -100,8 +100,9 @@ class TbCircleDataLayerItem extends TbLatestDataLayerItem, _dsData: FormattedData[]): void { + protected doInvalidateCoordinates(data: FormattedData, dsData: FormattedData[]): void { this.updateCircleShape(data); + this.updateLabel(data, dsData); } protected addItemClass(clazz: string): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts index df642a97cf..5c500335f3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts @@ -105,8 +105,9 @@ class TbPolygonDataLayerItem extends TbLatestDataLayerItem, _dsData: FormattedData[]): void { + protected doInvalidateCoordinates(data: FormattedData, dsData: FormattedData[]): void { this.updatePolygonShape(data); + this.updateLabel(data, dsData); } protected addItemClass(clazz: string): void { From ebfc12038daccb73505230bfa278d7f04e82344f Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 29 Apr 2026 16:56:47 +0300 Subject: [PATCH 23/25] env renaming --- application/src/main/resources/thingsboard.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 43a2a23207..cced6915be 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -61,11 +61,11 @@ server: # HTTP response compression compression: # Enable/disable HTTP response compression - enabled: "${SERVER_COMPRESSION_ENABLED:false}" + enabled: "${HTTP_COMPRESSION_ENABLED:false}" # Minimum size (in bytes) required for a response before compression is applied - min-response-size: "${SERVER_COMPRESSION_MIN_RESPONSE_SIZE:2048}" + min_response_size: "${HTTP_COMPRESSION_MIN_RESPONSE_SIZE:2048}" # Comma-separated list of MIME types that should be compressed - mime-types: "${SERVER_COMPRESSION_MIME_TYPES:text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json,application/xml}" + mime_types: "${HTTP_COMPRESSION_MIME_TYPES:text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json,application/xml}" # Log errors with stacktrace when REST API throws an exception with the message "Please contact sysadmin" log_controller_error_stack_trace: "${HTTP_LOG_CONTROLLER_ERROR_STACK_TRACE:false}" ws: From 4be45d447520aad7f261899a37462556baad4495 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 29 Apr 2026 17:07:22 +0200 Subject: [PATCH 24/25] Fixed CVE-2026-40895 --- msa/web-ui/yarn.lock | 6 +++--- ui-ngx/yarn.lock | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/msa/web-ui/yarn.lock b/msa/web-ui/yarn.lock index f421a62684..033aeac686 100644 --- a/msa/web-ui/yarn.lock +++ b/msa/web-ui/yarn.lock @@ -774,9 +774,9 @@ fn.name@1.x.x: integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== follow-redirects@^1.0.0: - version "1.15.11" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" - integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== + version "1.16.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.16.0.tgz#28474a159d3b9d11ef62050a14ed60e4df6d61bc" + integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw== forwarded@0.2.0: version "0.2.0" diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 4da832c885..e9564f5279 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -6240,9 +6240,9 @@ flatted@^3.2.9: resolved "https://github.com/thingsboard/flot.git#c2734540477d8b261d04ee18d4d38af3b0ecb81b" follow-redirects@^1.0.0: - version "1.15.9" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" - integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + version "1.16.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.16.0.tgz#28474a159d3b9d11ef62050a14ed60e4df6d61bc" + integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw== font-awesome@^4.7.0: version "4.7.0" From b794f6e22a7ed1b421fa8200ffe1b9ef41766598 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 30 Apr 2026 08:42:59 +0200 Subject: [PATCH 25/25] Force --force-confold/confdef for apt to fix tb-cassandra docker build The base image thingsboard/openjdk17:bookworm-slim ships a customized /etc/java-17-openjdk/security/java.security. When apt-get install pulls in a newer openjdk-17-jre-headless to satisfy cassandra's java11-runtime dependency, dpkg blocks on a non-interactive conffile prompt and the build fails. The ensuing "cassandra depends on java11-runtime" error is just the cascade from openjdk-17-jre-headless never finishing configure. Pass --force-confdef --force-confold so dpkg silently keeps the base image's customized conffile and the upgrade completes. --- msa/tb/docker-cassandra/Dockerfile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/msa/tb/docker-cassandra/Dockerfile b/msa/tb/docker-cassandra/Dockerfile index 34a90e6fb7..9570358c1e 100644 --- a/msa/tb/docker-cassandra/Dockerfile +++ b/msa/tb/docker-cassandra/Dockerfile @@ -42,6 +42,10 @@ ENV CASSANDRA_LOG=/var/log/cassandra COPY logback.xml ${pkg.name}.conf start-db.sh stop-db.sh start-tb.sh upgrade-tb.sh install-tb.sh ${pkg.name}.deb /tmp/ +# Keep base image's customized conffiles (e.g. /etc/java-17-openjdk/security/java.security) +# when apt upgrades openjdk-17-jre-headless transitively as cassandra's java11-runtime provider; +# without this dpkg blocks on a non-interactive conffile prompt and the build fails. +ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update \ && apt-get install -y --no-install-recommends wget nmap procps gnupg2 \ && echo "deb http://apt.postgresql.org/pub/repos/apt/ $(. /etc/os-release && echo -n $VERSION_CODENAME)-pgdg main" | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null \ @@ -49,7 +53,9 @@ RUN apt-get update \ && echo "deb https://debian.cassandra.apache.org 40x main" | tee -a /etc/apt/sources.list.d/cassandra.sources.list > /dev/null \ && wget -q https://downloads.apache.org/cassandra/KEYS -O- | apt-key add - \ && apt-get update \ - && apt-get install -y --no-install-recommends cassandra cassandra-tools postgresql-${PG_MAJOR} \ + && apt-get install -y --no-install-recommends \ + -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" \ + cassandra cassandra-tools postgresql-${PG_MAJOR} \ && rm -rf /var/lib/apt/lists/* \ && update-rc.d cassandra disable \ && update-rc.d postgresql disable \