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 3b7248ee72..081f1a9db5 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 @@ -106,26 +106,46 @@ public class DefaultCoapServerService implements CoapServerService, SmartInitial private CoapServer createCoapServer() throws UnknownHostException { Configuration networkConfig = createNetworkConfiguration(); server = new CoapServer(networkConfig); + try { + CoapEndpoint.Builder noSecCoapEndpointBuilder = new CoapEndpoint.Builder(); + InetAddress addr = InetAddress.getByName(coapServerContext.getHost()); + InetSocketAddress sockAddr = new InetSocketAddress(addr, coapServerContext.getPort()); + noSecCoapEndpointBuilder.setInetSocketAddress(sockAddr); + + noSecCoapEndpointBuilder.setConfiguration(networkConfig); + CoapEndpoint noSecCoapEndpoint = noSecCoapEndpointBuilder.build(); + server.addEndpoint(noSecCoapEndpoint); + if (isDtlsEnabled()) { + createDtlsEndpoint(networkConfig); + dtlsSessionsExecutor = ThingsBoardExecutors.newSingleThreadScheduledExecutor(getClass().getSimpleName()); + dtlsSessionsExecutor.scheduleAtFixedRate(this::evictTimeoutSessions, new Random().nextInt((int) getDtlsSessionReportTimeout()), getDtlsSessionReportTimeout(), TimeUnit.MILLISECONDS); + } + Resource root = server.getRoot(); + TbCoapServerMessageDeliverer messageDeliverer = new TbCoapServerMessageDeliverer(root); + server.setMessageDeliverer(messageDeliverer); - CoapEndpoint.Builder noSecCoapEndpointBuilder = new CoapEndpoint.Builder(); - InetAddress addr = InetAddress.getByName(coapServerContext.getHost()); - InetSocketAddress sockAddr = new InetSocketAddress(addr, coapServerContext.getPort()); - noSecCoapEndpointBuilder.setInetSocketAddress(sockAddr); - - noSecCoapEndpointBuilder.setConfiguration(networkConfig); - CoapEndpoint noSecCoapEndpoint = noSecCoapEndpointBuilder.build(); - server.addEndpoint(noSecCoapEndpoint); - if (isDtlsEnabled()) { - createDtlsEndpoint(networkConfig); - dtlsSessionsExecutor = ThingsBoardExecutors.newSingleThreadScheduledExecutor(getClass().getSimpleName()); - dtlsSessionsExecutor.scheduleAtFixedRate(this::evictTimeoutSessions, new Random().nextInt((int) getDtlsSessionReportTimeout()), getDtlsSessionReportTimeout(), TimeUnit.MILLISECONDS); + server.start(); + return server; + } catch (RuntimeException | UnknownHostException e) { + log.error("Failed to start CoAP server, releasing resources", e); + try { + if (dtlsSessionsExecutor != null) { + dtlsSessionsExecutor.shutdownNow(); + } + if (server != null) { + server.destroy(); + } + } catch (Exception suppressed) { + e.addSuppressed(suppressed); + } finally { + server = null; + dtlsSessionsExecutor = null; + dtlsConnector = null; + dtlsCoapEndpoint = null; + tbDtlsCertificateVerifier = null; + } + throw e; } - Resource root = server.getRoot(); - TbCoapServerMessageDeliverer messageDeliverer = new TbCoapServerMessageDeliverer(root); - server.setMessageDeliverer(messageDeliverer); - - server.start(); - return server; } private boolean isDtlsEnabled() { diff --git a/common/coap-server/src/test/java/org/thingsboard/server/coapserver/DefaultCoapServerServiceTest.java b/common/coap-server/src/test/java/org/thingsboard/server/coapserver/DefaultCoapServerServiceTest.java new file mode 100644 index 0000000000..5606fe18d7 --- /dev/null +++ b/common/coap-server/src/test/java/org/thingsboard/server/coapserver/DefaultCoapServerServiceTest.java @@ -0,0 +1,79 @@ +/** + * 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.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 java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class DefaultCoapServerServiceTest { + + private static final String HOST = "127.0.0.1"; + + @Mock + private CoapServerContext mockCoapServerContext; + + private DefaultCoapServerService service; + private DatagramSocket occupiedSocket; + private int occupiedPort; + + @BeforeEach + public void setUp() throws Exception { + occupiedSocket = new DatagramSocket(new InetSocketAddress(InetAddress.getByName(HOST), 0)); + occupiedPort = occupiedSocket.getLocalPort(); + + service = new DefaultCoapServerService(); + ReflectionTestUtils.setField(service, "coapServerContext", mockCoapServerContext); + + when(mockCoapServerContext.getHost()).thenReturn(HOST); + when(mockCoapServerContext.getPort()).thenReturn(occupiedPort); + when(mockCoapServerContext.getDtlsSettings()).thenReturn(null); + } + + @AfterEach + public void tearDown() { + if (occupiedSocket != null && !occupiedSocket.isClosed()) { + occupiedSocket.close(); + } + } + + @Test + public void whenPlainBindFails_thenInitThrowsAndReleasesCoapServer() { + assertThatThrownBy(() -> service.init()) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("None of the server endpoints could be started"); + + assertThat(ReflectionTestUtils.getField(service, "server")).isNull(); + assertThat(ReflectionTestUtils.getField(service, "dtlsSessionsExecutor")).isNull(); + assertThat(ReflectionTestUtils.getField(service, "dtlsConnector")).isNull(); + assertThat(ReflectionTestUtils.getField(service, "dtlsCoapEndpoint")).isNull(); + assertThat(ReflectionTestUtils.getField(service, "tbDtlsCertificateVerifier")).isNull(); + } + +} 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 9b370d0b71..bf2b48dd61 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 @@ -82,13 +82,29 @@ public class LwM2MTransportBootstrapService implements SmartInitializingSingleto @PostConstruct public void init() { log.info("Starting LwM2M transport bootstrap server..."); - this.server = getLhBootstrapServer(); - this.server.start(); - log.info("Started LwM2M transport bootstrap server."); + LeshanBootstrapServer bootstrapServer = getLhBootstrapServer(); + try { + this.server = bootstrapServer; + bootstrapServer.start(); + log.info("Started LwM2M transport bootstrap server."); + } catch (RuntimeException e) { + log.error("Failed to start LwM2M transport bootstrap server, releasing resources", e); + try { + bootstrapServer.destroy(); + } catch (Exception suppressed) { + e.addSuppressed(suppressed); + } finally { + this.server = null; + } + throw e; + } } @PreDestroy public void shutdown() { + if (server == null) { + return; + } try { log.info("Stopping LwM2M transport bootstrap server!"); server.destroy(); diff --git a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServiceTest.java b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServiceTest.java new file mode 100644 index 0000000000..67e8265361 --- /dev/null +++ b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServiceTest.java @@ -0,0 +1,115 @@ +/** + * 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.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.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.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 java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class LwM2MTransportBootstrapServiceTest { + + private static final String HOST = "127.0.0.1"; + + @Mock + private LwM2MTransportServerConfig serverConfig; + + @Mock + private LwM2MTransportBootstrapConfig bootstrapConfig; + + @Mock + private LwM2MBootstrapSecurityStore lwM2MBootstrapSecurityStore; + + @Mock + private LwM2MInMemoryBootstrapConfigStore lwM2MInMemoryBootstrapConfigStore; + + @Mock + private TransportService transportService; + + @Mock + private TbLwM2MDtlsBootstrapCertificateVerifier certificateVerifier; + + private LwM2MTransportBootstrapService service; + private DatagramSocket occupiedPlain; + private DatagramSocket occupiedSecure; + + @BeforeEach + public void setUp() throws Exception { + occupiedPlain = new DatagramSocket(new InetSocketAddress(InetAddress.getByName(HOST), 0)); + occupiedSecure = new DatagramSocket(new InetSocketAddress(InetAddress.getByName(HOST), 0)); + + when(bootstrapConfig.getHost()).thenReturn(HOST); + when(bootstrapConfig.getPort()).thenReturn(occupiedPlain.getLocalPort()); + when(bootstrapConfig.getSecureHost()).thenReturn(HOST); + when(bootstrapConfig.getSecurePort()).thenReturn(occupiedSecure.getLocalPort()); + when(bootstrapConfig.getSslCredentials()).thenReturn(null); + + when(serverConfig.isRecommendedCiphers()).thenReturn(false); + when(serverConfig.isRecommendedSupportedGroups()).thenReturn(false); + when(serverConfig.getDtlsRetransmissionTimeout()).thenReturn(9000); + when(serverConfig.getDtlsCidLength()).thenReturn(null); + + service = new LwM2MTransportBootstrapService( + serverConfig, + bootstrapConfig, + lwM2MBootstrapSecurityStore, + lwM2MInMemoryBootstrapConfigStore, + transportService, + certificateVerifier + ); + } + + @AfterEach + public void tearDown() { + if (occupiedPlain != null && !occupiedPlain.isClosed()) { + occupiedPlain.close(); + } + if (occupiedSecure != null && !occupiedSecure.isClosed()) { + occupiedSecure.close(); + } + } + + @Test + public void whenEndpointsFailToStart_thenInitThrowsAndReleasesBootstrapServer() { + assertThatThrownBy(() -> service.init()) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("None of the server endpoints could be started"); + + assertThat(ReflectionTestUtils.getField(service, "server")).isNull(); + } + +}