diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/Clip2Bridge.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/Clip2Bridge.java
index 2d7b52a92ed42..779fda3e37fea 100644
--- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/Clip2Bridge.java
+++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/Clip2Bridge.java
@@ -16,11 +16,18 @@
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.IOException;
+import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
+import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
@@ -28,7 +35,6 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
-import java.util.Properties;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
@@ -40,6 +46,8 @@
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
import javax.ws.rs.core.MediaType;
import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -81,7 +89,7 @@
import org.openhab.binding.hue.internal.exceptions.HttpUnauthorizedException;
import org.openhab.binding.hue.internal.handler.Clip2BridgeHandler;
import org.openhab.core.io.net.http.HttpClientFactory;
-import org.openhab.core.io.net.http.HttpUtil;
+import org.openhab.core.io.net.http.TrustAllTrustManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -532,11 +540,46 @@ public void close() {
* @throws NumberFormatException if the bridge firmware version is invalid.
*/
public static boolean isClip2Supported(String hostName) throws IOException {
- String response;
- Properties headers = new Properties();
- headers.put(HttpHeader.ACCEPT, MediaType.APPLICATION_JSON);
- response = HttpUtil.executeUrl("GET", String.format(FORMAT_URL_CONFIG, hostName), headers, null, null,
- TIMEOUT_SECONDS * 1000);
+ String response = null;
+ HttpURLConnection httpConnection = null;
+ HttpsURLConnection httpsConnection = null;
+ try {
+ URL url = new URI(String.format(FORMAT_URL_CONFIG, hostName)).toURL();
+ httpConnection = (HttpURLConnection) url.openConnection();
+ /*
+ * TODO we manually check if the bridge redirects to HTTPS, and if so, since v3 bridges
+ * currently don't provide a full certificate chain we force use of a TrustAllTrustManager
+ */
+ httpConnection.setInstanceFollowRedirects(false);
+ int status = httpConnection.getResponseCode();
+ if (status == 301 || status == 302) {
+ String redirectUrl = httpConnection.getHeaderField("Location");
+ if (redirectUrl != null && redirectUrl.startsWith("https://")) {
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, new TrustAllTrustManager[] { TrustAllTrustManager.getInstance() }, null);
+ httpsConnection = (HttpsURLConnection) new URI(redirectUrl).toURL().openConnection();
+ httpsConnection.setSSLSocketFactory(sslContext.getSocketFactory());
+ try (InputStream in = httpsConnection.getInputStream()) {
+ response = new String(in.readAllBytes(), StandardCharsets.UTF_8);
+ }
+ }
+ }
+ if (response == null) {
+ try (InputStream in = httpConnection.getInputStream()) {
+ response = new String(in.readAllBytes(), StandardCharsets.UTF_8);
+ }
+ }
+ } catch (NoSuchAlgorithmException | KeyManagementException | URISyntaxException e) {
+ throw new IOException("isClip2Supported() error connecting to bridge", e);
+ } finally {
+ if (httpConnection != null) {
+ httpConnection.disconnect();
+ }
+ if (httpsConnection != null) {
+ httpsConnection.disconnect();
+ }
+ }
+
BridgeConfig config = new Gson().fromJson(response, BridgeConfig.class);
if (Objects.nonNull(config)) {
String swVersion = config.swversion;
diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/HueTlsTrustManagerProvider.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/HueTlsTrustManagerProvider.java
index d34657c5484bd..9e98a16bf3603 100644
--- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/HueTlsTrustManagerProvider.java
+++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/HueTlsTrustManagerProvider.java
@@ -38,17 +38,34 @@
@NonNullByDefault
public class HueTlsTrustManagerProvider implements TlsTrustManagerProvider {
- private static final String PEM_FILENAME = "huebridge_cacert.pem";
+ private static final String PEM_CACERT_V1_FILENAME = "huebridge_cacert.pem";
+ private static final String PEM_CACERT_V2_FILENAME = "huebridge_cacert_v2.pem";
private final String hostname;
private final boolean useSelfSignedCertificate;
+ private final boolean isBridgeV3orHigher;
private final Logger logger = LoggerFactory.getLogger(HueTlsTrustManagerProvider.class);
- private @Nullable PEMTrustManager trustManager;
+ private @Nullable X509ExtendedTrustManager trustManager;
- public HueTlsTrustManagerProvider(String hostname, boolean useSelfSignedCertificate) {
+ /**
+ * Creates a new instance of {@link HueTlsTrustManagerProvider}.
+ *
+ * See the documentation for more details about 'Signify private CA Certificates V1 and V2 for Hue Bridges'.
+ *
+ * @see https://developers.meethue.com/develop/application-design-guidance/using-https/
+ *
+ * @param hostname the hostname of the Hue Bridge
+ * @param useSelfSignedCertificate true, to use the self-signed certificate downloaded from the Hue Bridge;
+ * false, to use the Signify private CA Certificate V1 or V2 for Hue Bridges from resources
+ * @param isBridgeV3orHigher true, to use the 'Signify private CA Certificate V2 for Hue Bridges';
+ * false, to use the 'Signify private CA Certificate V1 for Hue Bridges'
+ */
+ public HueTlsTrustManagerProvider(String hostname, boolean useSelfSignedCertificate, boolean isBridgeV3orHigher) {
this.hostname = hostname;
this.useSelfSignedCertificate = useSelfSignedCertificate;
+ this.isBridgeV3orHigher = isBridgeV3orHigher;
}
@Override
@@ -58,18 +75,25 @@ public String getHostName() {
@Override
public X509ExtendedTrustManager getTrustManager() {
- PEMTrustManager localTrustManager = getPEMTrustManager();
+ X509ExtendedTrustManager localTrustManager = getPEMTrustManager();
if (localTrustManager == null) {
logger.error("Cannot get the PEM certificate - returning a TrustAllTrustManager");
}
return localTrustManager != null ? localTrustManager : TrustAllTrustManager.getInstance();
}
- public @Nullable PEMTrustManager getPEMTrustManager() {
- PEMTrustManager localTrustManager = trustManager;
+ public @Nullable X509ExtendedTrustManager getPEMTrustManager() {
+ X509ExtendedTrustManager localTrustManager = trustManager;
if (localTrustManager != null) {
return localTrustManager;
}
+
+ // TODO V3 bridges currently don't provide the full certificate chain (missing intermediate certificate)
+ if (isBridgeV3orHigher) {
+ logger.error("Hue V3 Bridge has incomplete PEM certificate chains - defaulting to a TrustAllTrustManager");
+ return TrustAllTrustManager.getInstance();
+ }
+
try {
if (useSelfSignedCertificate) {
logger.trace("Use self-signed certificate downloaded from Hue Bridge.");
@@ -77,8 +101,9 @@ public X509ExtendedTrustManager getTrustManager() {
localTrustManager = PEMTrustManager.getInstanceFromServer("https://" + getHostName());
} else {
logger.trace("Use Signify private CA Certificate for Hue Bridges from resources.");
- // use Signify private CA Certificate for Hue Bridges from resources
- localTrustManager = getInstanceFromResource(PEM_FILENAME);
+ // use Signify private CA Certificate V1 or V2 for Hue Bridges from resources
+ localTrustManager = getInstanceFromResource(
+ isBridgeV3orHigher ? PEM_CACERT_V2_FILENAME : PEM_CACERT_V1_FILENAME);
}
this.trustManager = localTrustManager;
} catch (CertificateException | MalformedURLException e) {
diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeMDNSDiscoveryParticipant.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeMDNSDiscoveryParticipant.java
index e22689b7c3920..3ee8b3709816b 100644
--- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeMDNSDiscoveryParticipant.java
+++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeMDNSDiscoveryParticipant.java
@@ -19,6 +19,8 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import javax.jmdns.ServiceInfo;
@@ -54,6 +56,26 @@
@NonNullByDefault
public class HueBridgeMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant {
+ private static final Pattern BSB_MODEL_ID_PATTERN = Pattern.compile("^BSB(\\d{3})$");
+
+ /**
+ * Checks if the given model ID is a BSB model and if its version is 003 or above.
+ *
+ * @param modelId the model ID to check
+ * @return true if the model ID is a BSB model with version 003 or above, false otherwise
+ */
+ public static boolean modelIsOrAboveBSB003(@Nullable String modelId) {
+ if (modelId == null) {
+ return false;
+ }
+ Matcher matcher = BSB_MODEL_ID_PATTERN.matcher(modelId);
+ if (!matcher.matches()) {
+ return false;
+ }
+ int version = Integer.parseInt(matcher.group(1));
+ return version >= 3;
+ }
+
private static final String SERVICE_TYPE = "_hue._tcp.local.";
private static final String MDNS_PROPERTY_BRIDGE_ID = "bridgeid";
private static final String MDNS_PROPERTY_MODEL_ID = "modelid";
@@ -109,6 +131,7 @@ public String getServiceType() {
@Override
public @Nullable DiscoveryResult createResult(ServiceInfo service) {
+ logger.debug("Discovered mDNS service: {}", service.getNiceTextString());
if (isAutoDiscoveryEnabled) {
ThingUID uid = getThingUID(service);
if (Objects.nonNull(uid)) {
@@ -160,6 +183,9 @@ private Optional getLegacyBridge(String ipAddress) {
String id = service.getPropertyString(MDNS_PROPERTY_BRIDGE_ID);
if (id != null && !id.isBlank()) {
id = id.toLowerCase();
+ if (modelIsOrAboveBSB003(service.getPropertyString(MDNS_PROPERTY_MODEL_ID))) {
+ return new ThingUID(THING_TYPE_BRIDGE_API2, id);
+ }
try {
return Clip2Bridge.isClip2Supported(service.getHostAddresses()[0])
? new ThingUID(THING_TYPE_BRIDGE_API2, id)
diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java
index e8a7ccb51b799..59297d94b3c56 100644
--- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java
+++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java
@@ -45,6 +45,7 @@
import org.openhab.binding.hue.internal.connection.Clip2Bridge;
import org.openhab.binding.hue.internal.connection.HueTlsTrustManagerProvider;
import org.openhab.binding.hue.internal.discovery.Clip2ThingDiscoveryService;
+import org.openhab.binding.hue.internal.discovery.HueBridgeMDNSDiscoveryParticipant;
import org.openhab.binding.hue.internal.exceptions.ApiException;
import org.openhab.binding.hue.internal.exceptions.AssetNotLoadedException;
import org.openhab.binding.hue.internal.exceptions.HttpUnauthorizedException;
@@ -494,8 +495,10 @@ private void initializeAssets() {
return;
}
+ boolean useSignifyCaCertificateVersion2 = HueBridgeMDNSDiscoveryParticipant
+ .modelIsOrAboveBSB003(thing.getProperties().get(Thing.PROPERTY_MODEL_ID));
HueTlsTrustManagerProvider trustManagerProvider = new HueTlsTrustManagerProvider(ipAddress + ":443",
- config.useSelfSignedCertificate);
+ config.useSelfSignedCertificate, useSignifyCaCertificateVersion2);
if (Objects.isNull(trustManagerProvider.getPEMTrustManager())) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java
index 64f658fefea00..2f05ea889d845 100644
--- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java
+++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java
@@ -698,7 +698,7 @@ public void initialize() {
scheduler.submit(() -> {
// register trustmanager service
HueTlsTrustManagerProvider tlsTrustManagerProvider = new HueTlsTrustManagerProvider(
- ip + ":" + hueBridgeConfig.getPort(), hueBridgeConfig.useSelfSignedCertificate);
+ ip + ":" + hueBridgeConfig.getPort(), hueBridgeConfig.useSelfSignedCertificate, false);
// Check before registering that the PEM certificate can be downloaded
if (tlsTrustManagerProvider.getPEMTrustManager() == null) {
diff --git a/bundles/org.openhab.binding.hue/src/main/resources/huebridge_cacert_v2.pem b/bundles/org.openhab.binding.hue/src/main/resources/huebridge_cacert_v2.pem
new file mode 100644
index 0000000000000..081a58f82efbf
--- /dev/null
+++ b/bundles/org.openhab.binding.hue/src/main/resources/huebridge_cacert_v2.pem
@@ -0,0 +1,12 @@
+-----BEGIN CERTIFICATE-----
+MIIBzDCCAXOgAwIBAgICEAAwCgYIKoZIzj0EAwIwPDELMAkGA1UEBhMCTkwxFDAS
+BgNVBAoMC1NpZ25pZnkgSHVlMRcwFQYDVQQDDA5IdWUgUm9vdCBDQSAwMTAgFw0y
+NTAyMjUwMDAwMDBaGA8yMDUwMTIzMTIzNTk1OVowPDELMAkGA1UEBhMCTkwxFDAS
+BgNVBAoMC1NpZ25pZnkgSHVlMRcwFQYDVQQDDA5IdWUgUm9vdCBDQSAwMTBZMBMG
+ByqGSM49AgEGCCqGSM49AwEHA0IABFfOO0jfSAUXGQ9kjEDzyBrcMQ3ItyA5krE+
+cyvb1Y3xFti7KlAad8UOnAx0FBLn7HZrlmIwm1QnX0fK3LPM13mjYzBhMB0GA1Ud
+DgQWBBTF1pSpsCASX/z0VHLigxU2CAaqoTAfBgNVHSMEGDAWgBTF1pSpsCASX/z0
+VHLigxU2CAaqoTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAKBggq
+hkjOPQQDAgNHADBEAiAk7duT+IHbOGO4UUuGLAEpyYejGZK9Z7V9oSfnvuQ5BQIg
+IYSgwwxHXm73/JgcU9lAM6c8Bmu3UE3kBIUwBs1qXFw=
+-----END CERTIFICATE-----