Merge pull request #9145 from YevhenBondarenko/feature/mqtts-cert
download mqtts cert improvements
This commit is contained in:
		
						commit
						4e1e9dcc7d
					
				@ -27,6 +27,7 @@ import org.mockito.AdditionalAnswers;
 | 
			
		||||
import org.mockito.Mockito;
 | 
			
		||||
import org.springframework.context.annotation.Bean;
 | 
			
		||||
import org.springframework.context.annotation.Primary;
 | 
			
		||||
import org.springframework.test.annotation.DirtiesContext;
 | 
			
		||||
import org.springframework.test.context.ContextConfiguration;
 | 
			
		||||
import org.springframework.test.context.TestPropertySource;
 | 
			
		||||
import org.thingsboard.common.util.JacksonUtil;
 | 
			
		||||
@ -43,13 +44,15 @@ import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileCon
 | 
			
		||||
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
 | 
			
		||||
import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration;
 | 
			
		||||
import org.thingsboard.server.common.data.id.DeviceProfileId;
 | 
			
		||||
import org.thingsboard.server.common.data.page.PageData;
 | 
			
		||||
import org.thingsboard.server.common.data.security.Authority;
 | 
			
		||||
import org.thingsboard.server.common.data.security.DeviceCredentials;
 | 
			
		||||
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
 | 
			
		||||
import org.thingsboard.server.dao.device.DeviceDao;
 | 
			
		||||
import org.thingsboard.server.dao.service.DaoSqlTest;
 | 
			
		||||
 | 
			
		||||
import java.nio.file.Files;
 | 
			
		||||
import java.nio.file.Path;
 | 
			
		||||
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThat;
 | 
			
		||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 | 
			
		||||
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAP;
 | 
			
		||||
@ -59,21 +62,43 @@ import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTP;
 | 
			
		||||
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTPS;
 | 
			
		||||
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTT;
 | 
			
		||||
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTTS;
 | 
			
		||||
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.PEM_CERT_FILE_NAME;
 | 
			
		||||
 | 
			
		||||
@TestPropertySource(properties = {
 | 
			
		||||
        "device.connectivity.https.enabled=true",
 | 
			
		||||
        "device.connectivity.mqtts.enabled=true",
 | 
			
		||||
        "device.connectivity.mqtts.pem_cert_file=/tmp/" + PEM_CERT_FILE_NAME,
 | 
			
		||||
        "device.connectivity.coaps.enabled=true",
 | 
			
		||||
})
 | 
			
		||||
@ContextConfiguration(classes = {DeviceConnectivityControllerTest.Config.class})
 | 
			
		||||
@DaoSqlTest
 | 
			
		||||
public class DeviceConnectivityControllerTest extends AbstractControllerTest {
 | 
			
		||||
    static final TypeReference<PageData<Device>> PAGE_DATA_DEVICE_TYPE_REF = new TypeReference<>() {
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private static final String DEVICE_TELEMETRY_TOPIC = "v1/devices/customTopic";
 | 
			
		||||
    private static final String CHECK_DOCUMENTATION = "Check documentation";
 | 
			
		||||
 | 
			
		||||
    private static final String CERT = "-----BEGIN CERTIFICATE-----\n" +
 | 
			
		||||
            "MIIBfzCCASmgAwIBAgIUC1dtaskm/SJLmFE2Ae+YojArg+swDQYJKoZIhvcNAQEL\n" +
 | 
			
		||||
            "BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDgyODEzMTAzM1oXDTI0MDgy\n" +
 | 
			
		||||
            "NzEzMTAzM1owFDESMBAGA1UEAwwJbG9jYWxob3N0MFwwDQYJKoZIhvcNAQEBBQAD\n" +
 | 
			
		||||
            "SwAwSAJBANpcs46MavFdv7onsxH178YgK5XbpMqzx8AKaLMP2X6UEXN0nlt5mpX5\n" +
 | 
			
		||||
            "uCJmSwVaFn6lwTm8ThXFYOBydOQImIsCAwEAAaNTMFEwHQYDVR0OBBYEFDvN49bI\n" +
 | 
			
		||||
            "LaWMmUZ+cMboWAaozfXTMB8GA1UdIwQYMBaAFDvN49bILaWMmUZ+cMboWAaozfXT\n" +
 | 
			
		||||
            "MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADQQAhIQL8zPvIhQvHJocU\n" +
 | 
			
		||||
            "tnSmDAE0iR2rJVkousA+LiORE9BnuBtBUEv5SvFUv3VYUWA0eYFoyatpDHByIm6e\n" +
 | 
			
		||||
            "/+1c\n" +
 | 
			
		||||
            "-----END CERTIFICATE-----\n";
 | 
			
		||||
    private static final String P_KEY = "-----BEGIN PRIVATE KEY-----\n" +
 | 
			
		||||
            "MIIBVgIBADANBgkqhkiG9w0BAQEFAASCAUAwggE8AgEAAkEA2lyzjoxq8V2/uiez\n" +
 | 
			
		||||
            "EfXvxiArldukyrPHwAposw/ZfpQRc3SeW3malfm4ImZLBVoWfqXBObxOFcVg4HJ0\n" +
 | 
			
		||||
            "5AiYiwIDAQABAkEA1DYhPljSmc2dRcHNMphLtMWQ9iumpGRBrS2wgMzXdz2NF2+0\n" +
 | 
			
		||||
            "4cicaaL06/Cw6XXx43s8cn7e1xZAkGtNRQuqMQIhAPbrqrcYsropURpI5HSemeha\n" +
 | 
			
		||||
            "MJA3i67ZFaom39VSrNKJAiEA4mQ0qFKxFSh2xAOqDWDRkiCgdOS00J6hgrYJRPcI\n" +
 | 
			
		||||
            "nXMCIQDBHGjkT72gGKYkT3PUvSGTdc3bTIXDFmZ6L3MJTGJ7OQIhAKO+6r9coCy3\n" +
 | 
			
		||||
            "ib+ZDuSCRNK2upgR3B6Qvi020VmKfDa1AiBhCgpBlClv5OjnmC42EGxxFOaZtNQQ\n" +
 | 
			
		||||
            "C3swkUdrR3pezg==\n" +
 | 
			
		||||
            "-----END PRIVATE KEY-----\n";
 | 
			
		||||
 | 
			
		||||
    ListeningExecutorService executor;
 | 
			
		||||
 | 
			
		||||
    private Tenant savedTenant;
 | 
			
		||||
@ -337,4 +362,38 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
 | 
			
		||||
        assertThat(commands).hasSize(1);
 | 
			
		||||
        assertThat(commands.get(COAP).get(COAPS).asText()).isEqualTo(CHECK_DOCUMENTATION);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @DirtiesContext
 | 
			
		||||
    public void testDownloadMqttCert() throws Exception {
 | 
			
		||||
        Path path = Files.createFile(Path.of("/tmp/" + PEM_CERT_FILE_NAME));
 | 
			
		||||
        Files.writeString(path, CERT);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            String downloadedCert = doGet("/api/device-connectivity/mqtts/certificate/download", String.class);
 | 
			
		||||
            Assert.assertEquals(CERT, downloadedCert);
 | 
			
		||||
        } finally {
 | 
			
		||||
            Files.deleteIfExists(path);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @DirtiesContext
 | 
			
		||||
    public void testDownloadMqttCertFromFileWithPrivateKey() throws Exception {
 | 
			
		||||
        Path path = Files.createFile(Path.of("/tmp/" + PEM_CERT_FILE_NAME));
 | 
			
		||||
        Files.writeString(path, CERT + P_KEY);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            String downloadedCert = doGet("/api/device-connectivity/mqtts/certificate/download", String.class);
 | 
			
		||||
            Assert.assertEquals(CERT, downloadedCert);
 | 
			
		||||
        } finally {
 | 
			
		||||
            Files.deleteIfExists(path);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @DirtiesContext
 | 
			
		||||
    public void testDownloadMqttCertWithoutCertFile() throws Exception {
 | 
			
		||||
        doGet("/api/device-connectivity/mqtts/certificate/download").andExpect(status().isNotFound());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -79,6 +79,10 @@
 | 
			
		||||
            <groupId>org.postgresql</groupId>
 | 
			
		||||
            <artifactId>postgresql</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.bouncycastle</groupId>
 | 
			
		||||
            <artifactId>bcpkix-jdk15on</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.springframework.boot</groupId>
 | 
			
		||||
            <artifactId>spring-boot-starter-test</artifactId>
 | 
			
		||||
 | 
			
		||||
@ -19,13 +19,14 @@ import lombok.Data;
 | 
			
		||||
import org.springframework.boot.context.properties.ConfigurationProperties;
 | 
			
		||||
import org.springframework.context.annotation.Configuration;
 | 
			
		||||
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
@Configuration
 | 
			
		||||
@ConfigurationProperties(prefix = "device")
 | 
			
		||||
@Data
 | 
			
		||||
public class DeviceConnectivityConfiguration {
 | 
			
		||||
    private Map<String, DeviceConnectivityInfo> connectivity;
 | 
			
		||||
    private Map<String, DeviceConnectivityInfo> connectivity = new HashMap<>();
 | 
			
		||||
 | 
			
		||||
    public boolean isEnabled(String protocol) {
 | 
			
		||||
        var info = connectivity.get(protocol);
 | 
			
		||||
 | 
			
		||||
@ -19,8 +19,10 @@ import com.fasterxml.jackson.databind.JsonNode;
 | 
			
		||||
import com.fasterxml.jackson.databind.node.ArrayNode;
 | 
			
		||||
import com.fasterxml.jackson.databind.node.ObjectNode;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.bouncycastle.cert.X509CertificateHolder;
 | 
			
		||||
import org.bouncycastle.openssl.PEMParser;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Autowired;
 | 
			
		||||
import org.springframework.core.io.ClassPathResource;
 | 
			
		||||
import org.springframework.core.io.ByteArrayResource;
 | 
			
		||||
import org.springframework.core.io.Resource;
 | 
			
		||||
import org.springframework.stereotype.Service;
 | 
			
		||||
import org.thingsboard.common.util.JacksonUtil;
 | 
			
		||||
@ -35,11 +37,18 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
 | 
			
		||||
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
 | 
			
		||||
import org.thingsboard.server.dao.util.DeviceConnectivityUtil;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.PostConstruct;
 | 
			
		||||
import java.io.InputStream;
 | 
			
		||||
import java.io.InputStreamReader;
 | 
			
		||||
import java.net.URI;
 | 
			
		||||
import java.net.URISyntaxException;
 | 
			
		||||
import java.nio.charset.StandardCharsets;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.Base64;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.concurrent.ConcurrentHashMap;
 | 
			
		||||
 | 
			
		||||
import static org.thingsboard.server.dao.service.Validator.validateId;
 | 
			
		||||
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.CHECK_DOCUMENTATION;
 | 
			
		||||
@ -59,6 +68,8 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
 | 
			
		||||
    public static final String INCORRECT_DEVICE_ID = "Incorrect deviceId ";
 | 
			
		||||
    public static final String DEFAULT_DEVICE_TELEMETRY_TOPIC = "v1/devices/me/telemetry";
 | 
			
		||||
 | 
			
		||||
    private final Map<String, Resource> certs = new ConcurrentHashMap<>();
 | 
			
		||||
 | 
			
		||||
    @Autowired
 | 
			
		||||
    private DeviceCredentialsService deviceCredentialsService;
 | 
			
		||||
 | 
			
		||||
@ -68,6 +79,19 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
 | 
			
		||||
    @Autowired
 | 
			
		||||
    private DeviceConnectivityConfiguration deviceConnectivityConfiguration;
 | 
			
		||||
 | 
			
		||||
    @PostConstruct
 | 
			
		||||
    private void init() {
 | 
			
		||||
        DeviceConnectivityInfo mqtts = deviceConnectivityConfiguration.getConnectivity().get(MQTTS);
 | 
			
		||||
        if (mqtts != null && mqtts.isEnabled()) {
 | 
			
		||||
            String certFilePath = mqtts.getPemCertFile();
 | 
			
		||||
            if (StringUtils.isBlank(certFilePath) || !ResourceUtils.resourceExists(this, certFilePath)) {
 | 
			
		||||
                String error = StringUtils.isBlank(certFilePath) ? "path is empty" : "file is not exists";
 | 
			
		||||
                log.error("MQTTS is enabled but cert {}!", error);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public JsonNode findDevicePublishTelemetryCommands(String baseUrl, Device device) throws URISyntaxException {
 | 
			
		||||
        DeviceId deviceId = device.getId();
 | 
			
		||||
@ -115,15 +139,50 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Resource getPemCertFile(String protocol) {
 | 
			
		||||
        String certFilePath = deviceConnectivityConfiguration.getConnectivity()
 | 
			
		||||
                .get(protocol)
 | 
			
		||||
                .getPemCertFile();
 | 
			
		||||
        return certs.computeIfAbsent(protocol, key -> {
 | 
			
		||||
            String certFilePath = deviceConnectivityConfiguration.getConnectivity()
 | 
			
		||||
                    .get(protocol)
 | 
			
		||||
                    .getPemCertFile();
 | 
			
		||||
            if (StringUtils.isNotBlank(certFilePath) && ResourceUtils.resourceExists(this, certFilePath)) {
 | 
			
		||||
                try {
 | 
			
		||||
                    return getCert(certFilePath);
 | 
			
		||||
                } catch (Exception e) {
 | 
			
		||||
                    String msg = String.format("Failed to read %s server certificate!", protocol);
 | 
			
		||||
                    log.warn(msg);
 | 
			
		||||
                    throw new RuntimeException(msg, e);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        if (StringUtils.isNotBlank(certFilePath) && ResourceUtils.resourceExists(this, certFilePath)) {
 | 
			
		||||
            return new ClassPathResource(certFilePath);
 | 
			
		||||
        } else {
 | 
			
		||||
            return null;
 | 
			
		||||
    private Resource getCert(String path) throws Exception {
 | 
			
		||||
        StringBuilder pemContentBuilder = new StringBuilder();
 | 
			
		||||
 | 
			
		||||
        try (InputStream inStream = ResourceUtils.getInputStream(this, path);
 | 
			
		||||
             PEMParser pemParser = new PEMParser(new InputStreamReader(inStream))) {
 | 
			
		||||
 | 
			
		||||
            Object object;
 | 
			
		||||
 | 
			
		||||
            while ((object = pemParser.readObject()) != null) {
 | 
			
		||||
                if (object instanceof X509CertificateHolder) {
 | 
			
		||||
                    var certHolder = (X509CertificateHolder) object;
 | 
			
		||||
                    String certBase64 = Base64.getEncoder().encodeToString(certHolder.getEncoded());
 | 
			
		||||
 | 
			
		||||
                    pemContentBuilder.append("-----BEGIN CERTIFICATE-----\n");
 | 
			
		||||
                    int index = 0;
 | 
			
		||||
                    while (index < certBase64.length()) {
 | 
			
		||||
                        pemContentBuilder.append(certBase64, index, Math.min(index + 64, certBase64.length()));
 | 
			
		||||
                        pemContentBuilder.append("\n");
 | 
			
		||||
                        index += 64;
 | 
			
		||||
                    }
 | 
			
		||||
                    pemContentBuilder.append("-----END CERTIFICATE-----\n");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new ByteArrayResource(pemContentBuilder.toString().getBytes(StandardCharsets.UTF_8));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private JsonNode getHttpTransportPublishCommands(String defaultHostname, DeviceCredentials deviceCredentials) throws URISyntaxException {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user