Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,25 @@
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;
import java.util.Arrays;
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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -532,11 +540,38 @@ 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;
try {
URL url = new URI(String.format(FORMAT_URL_CONFIG, hostName)).toURL();
HttpURLConnection 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.startsWith("https://")) {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustAllTrustManager[] { TrustAllTrustManager.getInstance() }, null);
HttpsURLConnection httpsConnection = (HttpsURLConnection) new URI(redirectUrl).toURL()
.openConnection();
httpsConnection.setSSLSocketFactory(sslContext.getSocketFactory());
try (InputStream in = httpsConnection.getInputStream()) {
response = new String(in.readAllBytes());
}
}
}
if (response == null) {
try (InputStream in = httpConnection.getInputStream()) {
response = new String(in.readAllBytes());
}
}
} catch (NoSuchAlgorithmException | KeyManagementException | URISyntaxException e) {
throw new IOException("isClip2Supported() error connecting to bridge", e);
}

BridgeConfig config = new Gson().fromJson(response, BridgeConfig.class);
if (Objects.nonNull(config)) {
String swVersion = config.swversion;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,31 @@
@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 {@link https://developers.meethue.com/develop/application-design-guidance/using-https/} for more
* details about 'Signify private CA Certificates V1 and V2 for Hue Bridges'.
*
* @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
Expand All @@ -58,27 +72,35 @@ 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 .. default to a TrustAllTrustManager");
return TrustAllTrustManager.getInstance();
}

try {
if (useSelfSignedCertificate) {
logger.trace("Use self-signed certificate downloaded from Hue Bridge.");
// use self-signed certificate downloaded from Hue Bridge
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -160,6 +183,9 @@ private Optional<Thing> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIIBzDCCAXOgAwIBAgICEAAwCgYIKoZIzj0EAwIwPDELMAkGA1UEBhMCTkwxFDAS
BgNVBAoMC1NpZ25pZnkgSHVlMRcwFQYDVQQDDA5IdWUgUm9vdCBDQSAwMTAgFw0y
NTAyMjUwMDAwMDBaGA8yMDUwMTIzMTIzNTk1OVowPDELMAkGA1UEBhMCTkwxFDAS
BgNVBAoMC1NpZ25pZnkgSHVlMRcwFQYDVQQDDA5IdWUgUm9vdCBDQSAwMTBZMBMG
ByqGSM49AgEGCCqGSM49AwEHA0IABFfOO0jfSAUXGQ9kjEDzyBrcMQ3ItyA5krE+
cyvb1Y3xFti7KlAad8UOnAx0FBLn7HZrlmIwm1QnX0fK3LPM13mjYzBhMB0GA1Ud
DgQWBBTF1pSpsCASX/z0VHLigxU2CAaqoTAfBgNVHSMEGDAWgBTF1pSpsCASX/z0
VHLigxU2CAaqoTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAKBggq
hkjOPQQDAgNHADBEAiAk7duT+IHbOGO4UUuGLAEpyYejGZK9Z7V9oSfnvuQ5BQIg
IYSgwwxHXm73/JgcU9lAM6c8Bmu3UE3kBIUwBs1qXFw=
-----END CERTIFICATE-----
Loading