subscriber,
+ Runnable unsubscriber) {
+ this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, getter, setter, subscriber, unsubscriber);
+ }
}
diff --git a/src/main/java/io/github/hapjava/server/HomekitAccessoryCategories.java b/src/main/java/io/github/hapjava/server/HomekitAccessoryCategories.java
new file mode 100644
index 000000000..05768a947
--- /dev/null
+++ b/src/main/java/io/github/hapjava/server/HomekitAccessoryCategories.java
@@ -0,0 +1,40 @@
+package io.github.hapjava.server;
+
+public final class HomekitAccessoryCategories {
+ public static final int OTHER = 1;
+ public static final int BRIDGES = 2;
+ public static final int FANS = 3;
+ public static final int GARAGE_DOOR_OPENERS = 4;
+ public static final int LIGHTING = 5;
+ public static final int LOCKS = 6;
+ public static final int OUTLETS = 7;
+ public static final int SWITCHES = 8;
+ public static final int THERMOSTATS = 9;
+ public static final int SENSORS = 10;
+ public static final int SECURITY_SYSTEMS = 11;
+ public static final int DOORS = 12;
+ public static final int WINDOWS = 13;
+ public static final int WINDOW_COVERINGS = 14;
+ public static final int PROGRAMMABLE_SWITCHES = 15;
+ public static final int RANGE_EXTENDER = 16;
+ public static final int IP_CAMERAS = 17;
+ public static final int VIDEO_DOORBELLS = 18;
+ public static final int AIR_PURIFIERS = 19;
+ public static final int HEATERS = 20;
+ public static final int AIR_CONDITIONERS = 21;
+ public static final int HUMIDIFIERS = 22;
+ public static final int DEHUMIDIFIERS = 23;
+ public static final int APPLE_TV = 24;
+ public static final int HOMEPOD = 25;
+ public static final int SPEAKER = 26;
+ public static final int AIRPORT = 27;
+ public static final int SPRINKLERS = 28;
+ public static final int FAUCETS = 29;
+ public static final int SHOWER_SYSTEMS = 30;
+ public static final int TELEVISION = 31;
+ public static final int REMOTES = 32;
+ public static final int ROUTER = 33;
+ public static final int AUDIO_RECEIVER = 34;
+ public static final int TV_SET_TOP_BOX = 35;
+ public static final int TV_STREAMING_STICK = 36;
+}
diff --git a/src/main/java/io/github/hapjava/server/HomekitAuthInfo.java b/src/main/java/io/github/hapjava/server/HomekitAuthInfo.java
index 069bf7270..6441962d7 100644
--- a/src/main/java/io/github/hapjava/server/HomekitAuthInfo.java
+++ b/src/main/java/io/github/hapjava/server/HomekitAuthInfo.java
@@ -3,6 +3,8 @@
import io.github.hapjava.server.impl.HomekitServer;
import io.github.hapjava.server.impl.crypto.HAPSetupCodeUtils;
import java.math.BigInteger;
+import java.util.Collection;
+import java.util.List;
/**
* Authentication info that must be provided when constructing a new {@link HomekitServer}. You will
@@ -65,8 +67,23 @@ default String getSetupId() {
* @param username the iOS device's username. The value will not be meaningful to anything but
* iOS.
* @param publicKey the iOS device's public key.
+ * @param isAdmin if the user is an admin, authorized to and/remove other users
*/
- void createUser(String username, byte[] publicKey);
+ default void createUser(String username, byte[] publicKey, boolean isAdmin) {
+ createUser(username, publicKey);
+ }
+
+ /**
+ * Deprecated method to add a user, assuming all users are admins.
+ *
+ * At least one of the createUser methods must be implemented.
+ *
+ * @param username the iOS device's username.
+ * @param publicKey the iOS device's public key.
+ */
+ default void createUser(String username, byte[] publicKey) {
+ createUser(username, publicKey, true);
+ }
/**
* Called when an iOS device needs to remove an existing pairing. Subsequent calls to {@link
@@ -76,6 +93,15 @@ default String getSetupId() {
*/
void removeUser(String username);
+ /**
+ * List all users which have been authenticated.
+ *
+ * @return the previously stored list of users.
+ */
+ default Collection listUsers() {
+ return List.of();
+ }
+
/**
* Called when an already paired iOS device is re-connecting. The public key returned by this
* method will be compared with the signature of the pair verification request to validate the
@@ -86,6 +112,16 @@ default String getSetupId() {
*/
byte[] getUserPublicKey(String username);
+ /**
+ * Determine if the specified user is an admin.
+ *
+ * @param username the username of the iOS device to retrieve permissions for.
+ * @return the previously stored permissions.
+ */
+ default boolean userIsAdmin(String username) {
+ return true;
+ }
+
/**
* Called to check if a user has been created. The homekit accessory advertises whether the
* accessory has already been paired. At this time, it's unclear whether multiple users can be
diff --git a/src/main/java/io/github/hapjava/server/impl/HomekitRegistry.java b/src/main/java/io/github/hapjava/server/impl/HomekitRegistry.java
index 39ec31ff9..3c70cfb9e 100644
--- a/src/main/java/io/github/hapjava/server/impl/HomekitRegistry.java
+++ b/src/main/java/io/github/hapjava/server/impl/HomekitRegistry.java
@@ -2,6 +2,7 @@
import io.github.hapjava.accessories.HomekitAccessory;
import io.github.hapjava.characteristics.Characteristic;
+import io.github.hapjava.server.impl.connections.SubscriptionManager;
import io.github.hapjava.services.Service;
import io.github.hapjava.services.impl.AccessoryInformationService;
import java.util.ArrayList;
@@ -19,14 +20,16 @@ public class HomekitRegistry {
private static final Logger logger = LoggerFactory.getLogger(HomekitRegistry.class);
private final String label;
+ private final SubscriptionManager subscriptions;
private final Map accessories;
private final Map> services = new HashMap<>();
private final Map> characteristics =
new HashMap<>();
private boolean isAllowUnauthenticatedRequests = false;
- public HomekitRegistry(String label) {
+ public HomekitRegistry(String label, SubscriptionManager subscriptions) {
this.label = label;
+ this.subscriptions = subscriptions;
this.accessories = new ConcurrentHashMap<>();
reset();
}
@@ -39,8 +42,12 @@ public synchronized void reset() {
List newServices;
try {
newServices = new ArrayList<>(2);
- newServices.add(new AccessoryInformationService(accessory));
- for (Service service : accessory.getServices()) {
+ Collection services = accessory.getServices();
+ if (!services.stream()
+ .anyMatch(s -> s.getType().equals(AccessoryInformationService.TYPE))) {
+ newServices.add(new AccessoryInformationService(accessory));
+ }
+ for (Service service : services) {
newServices.add(service);
newServices.addAll(service.getLinkedServices());
}
@@ -61,6 +68,7 @@ public synchronized void reset() {
services.put(accessory, newServicesByInterfaceId);
characteristics.put(accessory, newCharacteristicsByInterfaceId);
}
+ subscriptions.resync(this);
}
public String getLabel() {
@@ -87,8 +95,8 @@ public void add(HomekitAccessory accessory) {
accessories.put(accessory.getId(), accessory);
}
- public void remove(HomekitAccessory accessory) {
- accessories.remove(accessory.getId());
+ public boolean remove(HomekitAccessory accessory) {
+ return accessories.remove(accessory.getId()) != null;
}
public boolean isAllowUnauthenticatedRequests() {
diff --git a/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java b/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java
index e619fa1e7..36bfeefa2 100644
--- a/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java
+++ b/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java
@@ -2,6 +2,7 @@
import io.github.hapjava.accessories.Bridge;
import io.github.hapjava.accessories.HomekitAccessory;
+import io.github.hapjava.server.HomekitAccessoryCategories;
import io.github.hapjava.server.HomekitAuthInfo;
import io.github.hapjava.server.HomekitWebHandler;
import io.github.hapjava.server.impl.connections.HomekitClientConnectionFactoryImpl;
@@ -25,24 +26,38 @@
public class HomekitRoot {
private static final Logger logger = LoggerFactory.getLogger(HomekitRoot.class);
-
+ private static final int DEFAULT_ACCESSORY_CATEGORY = HomekitAccessoryCategories.OTHER;
private final JmdnsHomekitAdvertiser advertiser;
private final HomekitWebHandler webHandler;
private final HomekitAuthInfo authInfo;
private final String label;
+ private final int category;
private final HomekitRegistry registry;
private final SubscriptionManager subscriptions = new SubscriptionManager();
private boolean started = false;
private int configurationIndex = 1;
+ private int nestedBatches = 0;
+ private boolean madeChanges = false;
+
+ HomekitRoot(
+ String label, HomekitWebHandler webHandler, InetAddress host, HomekitAuthInfo authInfo)
+ throws IOException {
+ this(label, DEFAULT_ACCESSORY_CATEGORY, webHandler, authInfo, new JmdnsHomekitAdvertiser(host));
+ }
HomekitRoot(
- String label, HomekitWebHandler webHandler, InetAddress localhost, HomekitAuthInfo authInfo)
+ String label,
+ int category,
+ HomekitWebHandler webHandler,
+ InetAddress host,
+ HomekitAuthInfo authInfo)
throws IOException {
- this(label, webHandler, authInfo, new JmdnsHomekitAdvertiser(localhost));
+ this(label, category, webHandler, authInfo, new JmdnsHomekitAdvertiser(host));
}
HomekitRoot(
String label,
+ int category,
HomekitWebHandler webHandler,
HomekitAuthInfo authInfo,
JmdnsHomekitAdvertiser advertiser)
@@ -51,19 +66,46 @@ public class HomekitRoot {
this.webHandler = webHandler;
this.authInfo = authInfo;
this.label = label;
- this.registry = new HomekitRegistry(label);
+ this.category = category;
+ this.registry = new HomekitRegistry(label, subscriptions);
+ }
+
+ HomekitRoot(
+ String label,
+ int category,
+ HomekitWebHandler webHandler,
+ JmDNS jmdns,
+ HomekitAuthInfo authInfo)
+ throws IOException {
+ this(label, category, webHandler, authInfo, new JmdnsHomekitAdvertiser(jmdns));
}
HomekitRoot(String label, HomekitWebHandler webHandler, JmDNS jmdns, HomekitAuthInfo authInfo)
throws IOException {
- this(label, webHandler, authInfo, new JmdnsHomekitAdvertiser(jmdns));
+ this(
+ label, DEFAULT_ACCESSORY_CATEGORY, webHandler, authInfo, new JmdnsHomekitAdvertiser(jmdns));
}
/**
- * Add an accessory to be handled and advertised by this root. Any existing HomeKit connections
- * will be terminated to allow the clients to reconnect and see the updated accessory list. When
- * using this for a bridge, the ID of the accessory must be greater than 1, as that ID is reserved
- * for the Bridge itself.
+ * Begin a batch update of accessories.
+ *
+ * After calling this, you can call addAccessory() and removeAccessory() multiple times without
+ * causing HAP-Java to re-publishing the metadata to HomeKit. You'll need to call
+ * completeUpdateBatch in order to publish all accumulated changes.
+ */
+ public synchronized void batchUpdate() {
+ if (this.nestedBatches == 0) madeChanges = false;
+ ++this.nestedBatches;
+ }
+
+ /** Publish accumulated accessory changes since batchUpdate() was called. */
+ public synchronized void completeUpdateBatch() {
+ if (--this.nestedBatches == 0 && madeChanges) registry.reset();
+ }
+
+ /**
+ * Add an accessory to be handled and advertised by this root. When using this for a bridge, the
+ * ID of the accessory must be greater than 1, as that ID is reserved for the Bridge itself.
*
* @param accessory to advertise and handle.
*/
@@ -86,27 +128,28 @@ void addAccessorySkipRangeCheck(HomekitAccessory accessory) {
if (logger.isTraceEnabled()) {
accessory.getName().thenAccept(name -> logger.trace("Added accessory {}", name));
}
- if (started) {
+ madeChanges = true;
+ if (started && nestedBatches == 0) {
registry.reset();
- webHandler.resetConnections();
}
}
/**
- * Removes an accessory from being handled or advertised by this root. Any existing HomeKit
- * connections will be terminated to allow the clients to reconnect and see the updated accessory
- * list.
+ * Removes an accessory from being handled or advertised by this root.
*
* @param accessory accessory to cease advertising and handling
*/
public void removeAccessory(HomekitAccessory accessory) {
- this.registry.remove(accessory);
- if (logger.isTraceEnabled()) {
- accessory.getName().thenAccept(name -> logger.trace("Removed accessory {}", name));
- }
- if (started) {
- registry.reset();
- webHandler.resetConnections();
+ if (this.registry.remove(accessory)) {
+ if (logger.isTraceEnabled()) {
+ accessory.getName().thenAccept(name -> logger.trace("Removed accessory {}", name));
+ }
+ madeChanges = true;
+ if (started && nestedBatches == 0) {
+ registry.reset();
+ }
+ } else {
+ accessory.getName().thenAccept(name -> logger.warn("Could not remove accessory {}", name));
}
}
@@ -118,6 +161,7 @@ public void removeAccessory(HomekitAccessory accessory) {
*/
public void start() {
started = true;
+ madeChanges = false;
registry.reset();
webHandler
.start(
@@ -127,7 +171,12 @@ public void start() {
try {
refreshAuthInfo();
advertiser.advertise(
- label, authInfo.getMac(), port, configurationIndex, authInfo.getSetupId());
+ label,
+ category,
+ authInfo.getMac(),
+ port,
+ configurationIndex,
+ authInfo.getSetupId());
} catch (Exception e) {
throw new RuntimeException(e);
}
@@ -148,6 +197,7 @@ public void stop() {
* @throws IOException if there is an error in the underlying protocol, such as a TCP error
*/
public void refreshAuthInfo() throws IOException {
+ advertiser.setMac(authInfo.getMac());
advertiser.setDiscoverable(!authInfo.hasUser());
}
diff --git a/src/main/java/io/github/hapjava/server/impl/HomekitServer.java b/src/main/java/io/github/hapjava/server/impl/HomekitServer.java
index d348ca682..7728896a2 100644
--- a/src/main/java/io/github/hapjava/server/impl/HomekitServer.java
+++ b/src/main/java/io/github/hapjava/server/impl/HomekitServer.java
@@ -118,6 +118,17 @@ public HomekitStandaloneAccessoryServer createStandaloneAccessory(
}
}
+ public HomekitStandaloneAccessoryServer createStandaloneAccessory(
+ HomekitAuthInfo authInfo, HomekitAccessory accessory, int category)
+ throws IOException, ExecutionException, InterruptedException {
+ if (jmdns != null) {
+ return new HomekitStandaloneAccessoryServer(accessory, http, jmdns, authInfo, category);
+ } else {
+ return new HomekitStandaloneAccessoryServer(
+ accessory, http, localAddress, authInfo, category);
+ }
+ }
+
/**
* Creates a bridge accessory, capable of holding multiple child accessories. This has the
* advantage over multiple standalone accessories of only requiring a single pairing from iOS for
@@ -139,6 +150,7 @@ public HomekitStandaloneAccessoryServer createStandaloneAccessory(
public HomekitRoot createBridge(
HomekitAuthInfo authInfo,
String label,
+ int category,
String manufacturer,
String model,
String serialNumber,
@@ -147,9 +159,9 @@ public HomekitRoot createBridge(
throws IOException {
HomekitRoot root;
if (jmdns != null) {
- root = new HomekitRoot(label, http, jmdns, authInfo);
+ root = new HomekitRoot(label, category, http, jmdns, authInfo);
} else {
- root = new HomekitRoot(label, http, localAddress, authInfo);
+ root = new HomekitRoot(label, category, http, localAddress, authInfo);
}
root.addAccessory(
new HomekitBridge(
diff --git a/src/main/java/io/github/hapjava/server/impl/HomekitStandaloneAccessoryServer.java b/src/main/java/io/github/hapjava/server/impl/HomekitStandaloneAccessoryServer.java
index 5eecfe45c..4a5dbabee 100644
--- a/src/main/java/io/github/hapjava/server/impl/HomekitStandaloneAccessoryServer.java
+++ b/src/main/java/io/github/hapjava/server/impl/HomekitStandaloneAccessoryServer.java
@@ -41,6 +41,28 @@ public class HomekitStandaloneAccessoryServer {
root.addAccessory(accessory);
}
+ HomekitStandaloneAccessoryServer(
+ HomekitAccessory accessory,
+ HomekitWebHandler webHandler,
+ InetAddress localhost,
+ HomekitAuthInfo authInfo,
+ int category)
+ throws UnknownHostException, IOException, ExecutionException, InterruptedException {
+ root = new HomekitRoot(accessory.getName().get(), category, webHandler, localhost, authInfo);
+ root.addAccessory(accessory);
+ }
+
+ HomekitStandaloneAccessoryServer(
+ HomekitAccessory accessory,
+ HomekitWebHandler webHandler,
+ JmDNS jmdns,
+ HomekitAuthInfo authInfo,
+ int category)
+ throws UnknownHostException, IOException, ExecutionException, InterruptedException {
+ root = new HomekitRoot(accessory.getName().get(), category, webHandler, jmdns, authInfo);
+ root.addAccessory(accessory);
+ }
+
/** Begins advertising and handling requests for this accessory. */
public void start() {
root.start();
diff --git a/src/main/java/io/github/hapjava/server/impl/connections/ConnectionImpl.java b/src/main/java/io/github/hapjava/server/impl/connections/ConnectionImpl.java
index ab2ff4444..759506722 100644
--- a/src/main/java/io/github/hapjava/server/impl/connections/ConnectionImpl.java
+++ b/src/main/java/io/github/hapjava/server/impl/connections/ConnectionImpl.java
@@ -27,6 +27,7 @@ class ConnectionImpl implements HomekitClientConnection {
private int outboundBinaryMessageCount = 0;
private byte[] readKey;
private byte[] writeKey;
+ private String username;
private boolean isUpgraded = false;
private final Consumer outOfBandMessageCallback;
private final SubscriptionManager subscriptions;
@@ -58,8 +59,9 @@ private HttpResponse doHandleRequest(HttpRequest request) throws IOException {
isUpgraded = true;
readKey = ((UpgradeResponse) response).getReadKey().array();
writeKey = ((UpgradeResponse) response).getWriteKey().array();
+ username = ((UpgradeResponse) response).getUsername();
}
- LOGGER.trace(response.getStatusCode() + " " + request.getUri());
+ LOGGER.trace("{} {} {}", response.getStatusCode(), request.getMethod(), request.getUri());
return response;
}
@@ -145,4 +147,9 @@ public void close() {
public void outOfBand(HttpResponse message) {
outOfBandMessageCallback.accept(message);
}
+
+ @Override
+ public String getUsername() {
+ return username;
+ }
}
diff --git a/src/main/java/io/github/hapjava/server/impl/connections/HttpSession.java b/src/main/java/io/github/hapjava/server/impl/connections/HttpSession.java
index 36ac04085..f1c97c62c 100644
--- a/src/main/java/io/github/hapjava/server/impl/connections/HttpSession.java
+++ b/src/main/java/io/github/hapjava/server/impl/connections/HttpSession.java
@@ -9,9 +9,9 @@
import io.github.hapjava.server.impl.jmdns.JmdnsHomekitAdvertiser;
import io.github.hapjava.server.impl.json.AccessoryController;
import io.github.hapjava.server.impl.json.CharacteristicsController;
-import io.github.hapjava.server.impl.pairing.PairVerificationManager;
-import io.github.hapjava.server.impl.pairing.PairingManager;
-import io.github.hapjava.server.impl.pairing.PairingUpdateController;
+import io.github.hapjava.server.impl.pairing.PairSetupManager;
+import io.github.hapjava.server.impl.pairing.PairVerifyManager;
+import io.github.hapjava.server.impl.pairing.PairingsManager;
import io.github.hapjava.server.impl.responses.InternalServerErrorResponse;
import io.github.hapjava.server.impl.responses.NotFoundResponse;
import java.io.IOException;
@@ -21,8 +21,8 @@
class HttpSession {
- private volatile PairingManager pairingManager;
- private volatile PairVerificationManager pairVerificationManager;
+ private volatile PairSetupManager pairSetupManager;
+ private volatile PairVerifyManager pairVerifyManager;
private volatile AccessoryController accessoryController;
private volatile CharacteristicsController characteristicsController;
@@ -67,7 +67,7 @@ public HttpResponse handleRequest(HttpRequest request) throws IOException {
public HttpResponse handleAuthenticatedRequest(HttpRequest request) throws IOException {
advertiser.setDiscoverable(
- false); // brigde is already bound and should not be discoverable anymore
+ false); // bridge is already bound and should not be discoverable anymore
try {
switch (request.getUri()) {
case "/accessories":
@@ -84,7 +84,7 @@ public HttpResponse handleAuthenticatedRequest(HttpRequest request) throws IOExc
}
case "/pairings":
- return new PairingUpdateController(authInfo, advertiser).handle(request);
+ return new PairingsManager(authInfo, advertiser).handle(request);
default:
if (request.getUri().startsWith("/characteristics?")) {
@@ -100,15 +100,15 @@ public HttpResponse handleAuthenticatedRequest(HttpRequest request) throws IOExc
}
private HttpResponse handlePairSetup(HttpRequest request) {
- if (pairingManager == null) {
+ if (pairSetupManager == null) {
synchronized (HttpSession.class) {
- if (pairingManager == null) {
- pairingManager = new PairingManager(authInfo, registry);
+ if (pairSetupManager == null) {
+ pairSetupManager = new PairSetupManager(authInfo, registry);
}
}
}
try {
- return pairingManager.handle(request);
+ return pairSetupManager.handle(request);
} catch (Exception e) {
logger.warn("Exception encountered during pairing", e);
return new InternalServerErrorResponse(e);
@@ -116,15 +116,15 @@ private HttpResponse handlePairSetup(HttpRequest request) {
}
private HttpResponse handlePairVerify(HttpRequest request) {
- if (pairVerificationManager == null) {
+ if (pairVerifyManager == null) {
synchronized (HttpSession.class) {
- if (pairVerificationManager == null) {
- pairVerificationManager = new PairVerificationManager(authInfo, registry);
+ if (pairVerifyManager == null) {
+ pairVerifyManager = new PairVerifyManager(authInfo, registry);
}
}
}
try {
- return pairVerificationManager.handle(request);
+ return pairVerifyManager.handle(request);
} catch (Exception e) {
logger.warn("Exception encountered while verifying pairing", e);
return new InternalServerErrorResponse(e);
diff --git a/src/main/java/io/github/hapjava/server/impl/connections/LengthPrefixedByteArrayProcessor.java b/src/main/java/io/github/hapjava/server/impl/connections/LengthPrefixedByteArrayProcessor.java
index 124acbb51..34ec3a41c 100644
--- a/src/main/java/io/github/hapjava/server/impl/connections/LengthPrefixedByteArrayProcessor.java
+++ b/src/main/java/io/github/hapjava/server/impl/connections/LengthPrefixedByteArrayProcessor.java
@@ -21,9 +21,9 @@ public synchronized Collection handle(byte[] data) {
int pos = 0;
LOGGER.trace(
"Received message of length {}. Existing buffer is {}", data.length, buffer.size());
- if (buffer.size() == 0) {
+ if (buffer.size() == 0 && firstLengthByteBuffer == null) {
while (data.length - pos > 18) {
- int targetLength = (data[0] & 0xFF) + (data[1] & 0xFF) * 256 + 16 + 2;
+ int targetLength = (data[pos] & 0xFF) + (data[pos + 1] & 0xFF) * 256 + 16 + 2;
LOGGER.trace("Attempting to read message of length {}", targetLength);
if (data.length >= pos + targetLength) {
byte[] b = new byte[targetLength - 2];
@@ -56,6 +56,7 @@ private void step(byte[] data, int pos, Collection results) {
if (targetLength == 0) {
if (firstLengthByteBuffer != null) {
targetLength = (firstLengthByteBuffer & 0xFF) + (data[pos] & 0xFF) * 256 + 16;
+ firstLengthByteBuffer = null;
pos += 1;
LOGGER.trace(
"Received the second byte after storing the first byte. New length is {}",
diff --git a/src/main/java/io/github/hapjava/server/impl/connections/SubscriptionManager.java b/src/main/java/io/github/hapjava/server/impl/connections/SubscriptionManager.java
index 25f68526c..9399295fb 100644
--- a/src/main/java/io/github/hapjava/server/impl/connections/SubscriptionManager.java
+++ b/src/main/java/io/github/hapjava/server/impl/connections/SubscriptionManager.java
@@ -1,15 +1,18 @@
package io.github.hapjava.server.impl.connections;
+import io.github.hapjava.characteristics.Characteristic;
import io.github.hapjava.characteristics.EventableCharacteristic;
+import io.github.hapjava.server.impl.HomekitRegistry;
import io.github.hapjava.server.impl.http.HomekitClientConnection;
import io.github.hapjava.server.impl.http.HttpResponse;
import io.github.hapjava.server.impl.json.EventController;
import java.util.ArrayList;
-import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -17,12 +20,22 @@ public class SubscriptionManager {
private static final Logger LOGGER = LoggerFactory.getLogger(SubscriptionManager.class);
- private final ConcurrentMap> subscriptions =
- new ConcurrentHashMap<>();
- private final ConcurrentMap> reverse =
- new ConcurrentHashMap<>();
- private final ConcurrentMap>
- pendingNotifications = new ConcurrentHashMap<>();
+ private static class ConnectionsWithIds {
+ Set connections;
+ int aid, iid;
+
+ ConnectionsWithIds(int aid, int iid) {
+ this.aid = aid;
+ this.iid = iid;
+ this.connections = new HashSet<>();
+ }
+ }
+
+ private final Map subscriptions = new HashMap<>();
+ private final Map> reverse =
+ new HashMap<>();
+ private final Map> pendingNotifications =
+ new HashMap<>();
private int nestedBatches = 0;
public synchronized void addSubscription(
@@ -31,96 +44,111 @@ public synchronized void addSubscription(
EventableCharacteristic characteristic,
HomekitClientConnection connection) {
synchronized (this) {
- if (!subscriptions.containsKey(characteristic)) {
- subscriptions.putIfAbsent(characteristic, newSet());
- }
- subscriptions.get(characteristic).add(connection);
- if (subscriptions.get(characteristic).size() == 1) {
- characteristic.subscribe(
- () -> {
- publish(aid, iid, characteristic);
- });
+ ConnectionsWithIds subscribers;
+ if (subscriptions.containsKey(characteristic)) {
+ subscribers = subscriptions.get(characteristic);
+ } else {
+ subscribers = new ConnectionsWithIds(aid, iid);
+ subscriptions.put(characteristic, subscribers);
+ subscribe(aid, iid, characteristic);
}
+ subscribers.connections.add(connection);
if (!reverse.containsKey(connection)) {
- reverse.putIfAbsent(connection, newSet());
+ reverse.put(connection, new HashSet<>());
}
reverse.get(connection).add(characteristic);
LOGGER.trace(
- "Added subscription to " + characteristic.getClass() + " for " + connection.hashCode());
+ "Added subscription to {}:{} ({}) for {}",
+ aid,
+ iid,
+ characteristic.getClass().getSimpleName(),
+ connection.hashCode());
}
}
public synchronized void removeSubscription(
EventableCharacteristic characteristic, HomekitClientConnection connection) {
- Set subscriptions = this.subscriptions.get(characteristic);
- if (subscriptions != null) {
- subscriptions.remove(connection);
- if (subscriptions.size() == 0) {
+ ConnectionsWithIds subscribers = subscriptions.get(characteristic);
+ if (subscribers != null) {
+ subscribers.connections.remove(connection);
+ if (subscribers.connections.isEmpty()) {
+ LOGGER.trace("Unsubscribing from characteristic as all subscriptions are closed");
characteristic.unsubscribe();
+ subscriptions.remove(characteristic);
+ }
+
+ // Remove pending notifications for this no-longer-subscribed characteristic
+ List connectionNotifications = pendingNotifications.get(connection);
+ if (connectionNotifications != null) {
+ connectionNotifications.removeIf(n -> n.aid == subscribers.aid && n.iid == subscribers.iid);
+ if (connectionNotifications.isEmpty()) pendingNotifications.remove(connection);
}
+
+ LOGGER.trace(
+ "Removed subscription from {}:{} ({}) for {}",
+ subscribers.aid,
+ subscribers.iid,
+ characteristic.getClass().getSimpleName(),
+ connection.hashCode());
}
Set reverse = this.reverse.get(connection);
if (reverse != null) {
reverse.remove(characteristic);
+ if (reverse.isEmpty()) this.reverse.remove(connection);
}
- LOGGER.trace(
- "Removed subscription to " + characteristic.getClass() + " for " + connection.hashCode());
}
public synchronized void removeConnection(HomekitClientConnection connection) {
- Set characteristics = reverse.remove(connection);
+ removeConnection(connection, reverse.remove(connection));
+ }
+
+ private void removeConnection(
+ HomekitClientConnection connection, Set characteristics) {
pendingNotifications.remove(connection);
if (characteristics != null) {
for (EventableCharacteristic characteristic : characteristics) {
- Set characteristicSubscriptions =
- subscriptions.get(characteristic);
- characteristicSubscriptions.remove(connection);
- if (characteristicSubscriptions.isEmpty()) {
- LOGGER.trace("Unsubscribing from characteristic as all subscriptions are closed");
- characteristic.unsubscribe();
- subscriptions.remove(characteristic);
- }
+ removeSubscription(characteristic, connection);
}
}
LOGGER.trace("Removed connection {}", connection.hashCode());
}
- private Set newSet() {
- return Collections.newSetFromMap(new ConcurrentHashMap());
- }
-
public synchronized void batchUpdate() {
++this.nestedBatches;
}
public synchronized void completeUpdateBatch() {
- if (--this.nestedBatches == 0 && !pendingNotifications.isEmpty()) {
- LOGGER.trace("Publishing batched changes");
- for (ConcurrentMap.Entry> entry :
- pendingNotifications.entrySet()) {
- try {
- HttpResponse message = new EventController().getMessage(entry.getValue());
- entry.getKey().outOfBand(message);
- } catch (Exception e) {
- LOGGER.warn("Failed to create new event message", e);
- }
+ if (--this.nestedBatches == 0) flushUpdateBatch();
+ }
+
+ private void flushUpdateBatch() {
+ if (pendingNotifications.isEmpty()) return;
+
+ LOGGER.trace("Publishing batched changes");
+ for (Map.Entry> entry :
+ pendingNotifications.entrySet()) {
+ try {
+ HttpResponse message = new EventController().getMessage(entry.getValue());
+ entry.getKey().outOfBand(message);
+ } catch (Exception e) {
+ LOGGER.warn("Failed to create new event message", e);
}
- pendingNotifications.clear();
}
+ pendingNotifications.clear();
}
public synchronized void publish(int accessoryId, int iid, EventableCharacteristic changed) {
- final Set subscribers = subscriptions.get(changed);
- if ((subscribers == null) || (subscribers.isEmpty())) {
- LOGGER.debug("No subscribers to characteristic {} at accessory {} ", changed, accessoryId);
+ final ConnectionsWithIds subscribers = subscriptions.get(changed);
+ if (subscribers == null || subscribers.connections.isEmpty()) {
+ LOGGER.trace("No subscribers to characteristic {} at accessory {} ", changed, accessoryId);
return; // no subscribers
}
if (nestedBatches != 0) {
LOGGER.trace("Batching change for accessory {} and characteristic {} " + accessoryId, iid);
PendingNotification notification = new PendingNotification(accessoryId, iid, changed);
- for (HomekitClientConnection connection : subscribers) {
+ for (HomekitClientConnection connection : subscribers.connections) {
if (!pendingNotifications.containsKey(connection)) {
pendingNotifications.put(connection, new ArrayList());
}
@@ -132,7 +160,7 @@ public synchronized void publish(int accessoryId, int iid, EventableCharacterist
try {
HttpResponse message = new EventController().getMessage(accessoryId, iid, changed);
LOGGER.trace("Publishing change for " + accessoryId);
- for (HomekitClientConnection connection : subscribers) {
+ for (HomekitClientConnection connection : subscribers.connections) {
connection.outOfBand(message);
}
} catch (Exception e) {
@@ -140,14 +168,89 @@ public synchronized void publish(int accessoryId, int iid, EventableCharacterist
}
}
+ /**
+ * The accessory registry has changed; go through all subscriptions and link to any new/changed
+ * characteristics
+ */
+ public synchronized void resync(HomekitRegistry registry) {
+ LOGGER.trace("Resyncing subscriptions");
+ flushUpdateBatch();
+
+ Map newSubscriptions = new HashMap<>();
+ Iterator> i =
+ subscriptions.entrySet().iterator();
+ while (i.hasNext()) {
+ Map.Entry entry = i.next();
+ EventableCharacteristic oldCharacteristic = entry.getKey();
+ ConnectionsWithIds subscribers = entry.getValue();
+ Characteristic newCharacteristic =
+ registry.getCharacteristics(subscribers.aid).get(subscribers.iid);
+ if (newCharacteristic == null || newCharacteristic.getType() != oldCharacteristic.getType()) {
+ // characteristic is gone or has completely changed; drop all subscriptions for it
+ LOGGER.trace(
+ "{}:{} ({}) has gone missing; dropping subscriptions.",
+ subscribers.aid,
+ subscribers.iid,
+ oldCharacteristic.getClass().getSimpleName());
+ i.remove();
+ for (HomekitClientConnection conn : subscribers.connections) {
+ removeSubscription(oldCharacteristic, conn);
+ }
+ } else if (newCharacteristic != oldCharacteristic) {
+ EventableCharacteristic newEventableCharacteristic =
+ (EventableCharacteristic) newCharacteristic;
+ LOGGER.trace(
+ "{}:{} has been replaced by a compatible characteristic; re-connecting subscriptions",
+ subscribers.aid,
+ subscribers.iid);
+ // characteristic has been replaced by another instance of the same thing;
+ // re-connect subscriptions
+ i.remove();
+ oldCharacteristic.unsubscribe();
+ subscribe(subscribers.aid, subscribers.iid, newEventableCharacteristic);
+ // we can't replace the key, and we can't add to the map while we're iterating,
+ // so save it off to a temporary map that we'll add them all at the end
+ newSubscriptions.put(newEventableCharacteristic, subscribers);
+
+ for (HomekitClientConnection conn : subscribers.connections) {
+ Set subscribedCharacteristics = reverse.get(conn);
+ subscribedCharacteristics.remove(oldCharacteristic);
+ subscribedCharacteristics.add(newEventableCharacteristic);
+
+ // and also update references for any pending notifications, so they'll get the proper
+ // value
+ List connectionPendingNotifications = pendingNotifications.get(conn);
+ if (connectionPendingNotifications != null) {
+ for (PendingNotification notification : connectionPendingNotifications) {
+ if (notification.characteristic == oldCharacteristic) {
+ notification.characteristic = newEventableCharacteristic;
+ }
+ }
+ }
+ }
+ }
+ }
+ subscriptions.putAll(newSubscriptions);
+ }
+
+ private void subscribe(int aid, int iid, EventableCharacteristic characteristic) {
+ characteristic.subscribe(
+ () -> {
+ publish(aid, iid, characteristic);
+ });
+ }
+
/** Remove all existing subscriptions */
- public void removeAll() {
+ public synchronized void removeAll() {
LOGGER.trace("Removing {} reverse connections from subscription manager", reverse.size());
- Iterator i = reverse.keySet().iterator();
+ Iterator>> i =
+ reverse.entrySet().iterator();
while (i.hasNext()) {
- HomekitClientConnection connection = i.next();
+ Map.Entry> entry = i.next();
+ HomekitClientConnection connection = entry.getKey();
LOGGER.trace("Removing connection {}", connection.hashCode());
- removeConnection(connection);
+ i.remove();
+ removeConnection(connection, entry.getValue());
}
LOGGER.trace("Subscription sizes are {} and {}", reverse.size(), subscriptions.size());
}
diff --git a/src/main/java/io/github/hapjava/server/impl/crypto/ChachaDecoder.java b/src/main/java/io/github/hapjava/server/impl/crypto/ChachaDecoder.java
old mode 100644
new mode 100755
index 4c74a469d..014f4b500
--- a/src/main/java/io/github/hapjava/server/impl/crypto/ChachaDecoder.java
+++ b/src/main/java/io/github/hapjava/server/impl/crypto/ChachaDecoder.java
@@ -1,54 +1,98 @@
package io.github.hapjava.server.impl.crypto;
import java.io.IOException;
-import org.bouncycastle.crypto.engines.ChaChaEngine;
-import org.bouncycastle.crypto.generators.Poly1305KeyGenerator;
+import org.bouncycastle.crypto.InvalidCipherTextException;
+import org.bouncycastle.crypto.modes.ChaCha20Poly1305;
+import org.bouncycastle.crypto.params.AEADParameters;
import org.bouncycastle.crypto.params.KeyParameter;
-import org.bouncycastle.crypto.params.ParametersWithIV;
-import org.bouncycastle.crypto.tls.AlertDescription;
-import org.bouncycastle.crypto.tls.TlsFatalAlert;
-import org.bouncycastle.util.Arrays;
+import org.bouncycastle.tls.AlertDescription;
+import org.bouncycastle.tls.TlsFatalAlert;
public class ChachaDecoder {
- private final ChaChaEngine decryptCipher;
+ private final ChaCha20Poly1305 cipher;
+ private final byte[] key;
+ private final byte[] nonce;
public ChachaDecoder(byte[] key, byte[] nonce) throws IOException {
+ this.key = key;
+ // ChaCha20-Poly1305 requires exactly 12 bytes (96 bits) for nonce
+ this.nonce = ensureNonceSize(nonce);
+ this.cipher = new ChaCha20Poly1305();
+ }
- this.decryptCipher = new ChaChaEngine(20);
+ private byte[] ensureNonceSize(byte[] nonce) {
+ if (nonce == null) {
+ return new byte[12]; // Default to zero nonce if null
+ }
- this.decryptCipher.init(false, new ParametersWithIV(new KeyParameter(key), nonce));
+ // For HomeKit pairing messages, handle Apple's string-based nonces
+ if (nonce.length == 8) {
+ // Apple's HomeKit implementation uses a specific nonce format
+ // Based on RFC 7539 and Apple's implementation, the nonce should be:
+ // - 4 bytes constant (0x00000000)
+ // - 8 bytes nonce string
+ // This matches ChaCha20's 96-bit nonce requirement
+ byte[] adjustedNonce = new byte[12];
+ // Put the 8-byte nonce at the END (bytes 4-11), not at the beginning
+ System.arraycopy(nonce, 0, adjustedNonce, 4, 8);
+ // First 4 bytes remain zero (counter initialization)
+ return adjustedNonce;
+ }
+
+ if (nonce.length == 12) {
+ return nonce; // Already correct size
+ }
+
+ // For other nonce lengths, pad or truncate to 12 bytes
+ byte[] adjustedNonce = new byte[12];
+ if (nonce.length < 12) {
+ // Pad with zeros if too short - put nonce at beginning
+ System.arraycopy(nonce, 0, adjustedNonce, 0, nonce.length);
+ // Remaining bytes are already zero
+ } else {
+ // Truncate if too long - take first 12 bytes
+ System.arraycopy(nonce, 0, adjustedNonce, 0, 12);
+ }
+
+ return adjustedNonce;
}
public byte[] decodeCiphertext(byte[] receivedMAC, byte[] additionalData, byte[] ciphertext)
throws IOException {
- KeyParameter macKey = initRecordMAC(decryptCipher);
+ try {
+ byte[] ciphertextWithTag = new byte[ciphertext.length + receivedMAC.length];
+ System.arraycopy(ciphertext, 0, ciphertextWithTag, 0, ciphertext.length);
+ System.arraycopy(receivedMAC, 0, ciphertextWithTag, ciphertext.length, receivedMAC.length);
+
+ ChaCha20Poly1305 cipher1 = new ChaCha20Poly1305();
+ AEADParameters params = new AEADParameters(new KeyParameter(key), 128, nonce, additionalData);
+ cipher1.init(false, params);
- byte[] calculatedMAC = PolyKeyCreator.create(macKey, additionalData, ciphertext);
+ byte[] output = new byte[cipher1.getOutputSize(ciphertextWithTag.length)];
+ int len = cipher1.processBytes(ciphertextWithTag, 0, ciphertextWithTag.length, output, 0);
+ len += cipher1.doFinal(output, len);
- if (!Arrays.constantTimeAreEqual(calculatedMAC, receivedMAC)) {
+ byte[] result = new byte[len];
+ System.arraycopy(output, 0, result, 0, len);
+
+ return result;
+
+ } catch (InvalidCipherTextException e) {
throw new TlsFatalAlert(AlertDescription.bad_record_mac);
}
+ }
- byte[] output = new byte[ciphertext.length];
- decryptCipher.processBytes(ciphertext, 0, ciphertext.length, output, 0);
-
- return output;
+ private static String bytesToHex(byte[] bytes) {
+ StringBuilder result = new StringBuilder();
+ for (byte b : bytes) {
+ result.append(String.format("%02x", b));
+ }
+ return result.toString();
}
public byte[] decodeCiphertext(byte[] receivedMAC, byte[] ciphertext) throws IOException {
return decodeCiphertext(receivedMAC, null, ciphertext);
}
-
- private KeyParameter initRecordMAC(ChaChaEngine cipher) {
- byte[] firstBlock = new byte[64];
- cipher.processBytes(firstBlock, 0, firstBlock.length, firstBlock, 0);
-
- // NOTE: The BC implementation puts 'r' after 'k'
- System.arraycopy(firstBlock, 0, firstBlock, 32, 16);
- KeyParameter macKey = new KeyParameter(firstBlock, 16, 32);
- Poly1305KeyGenerator.clamp(macKey.getKey());
- return macKey;
- }
}
diff --git a/src/main/java/io/github/hapjava/server/impl/crypto/ChachaEncoder.java b/src/main/java/io/github/hapjava/server/impl/crypto/ChachaEncoder.java
old mode 100644
new mode 100755
index 3304e7d9a..889c9ca86
--- a/src/main/java/io/github/hapjava/server/impl/crypto/ChachaEncoder.java
+++ b/src/main/java/io/github/hapjava/server/impl/crypto/ChachaEncoder.java
@@ -1,20 +1,64 @@
package io.github.hapjava.server.impl.crypto;
import java.io.IOException;
-import org.bouncycastle.crypto.engines.ChaChaEngine;
-import org.bouncycastle.crypto.generators.Poly1305KeyGenerator;
+import org.bouncycastle.crypto.InvalidCipherTextException;
+import org.bouncycastle.crypto.modes.ChaCha20Poly1305;
+import org.bouncycastle.crypto.params.AEADParameters;
import org.bouncycastle.crypto.params.KeyParameter;
-import org.bouncycastle.crypto.params.ParametersWithIV;
public class ChachaEncoder {
- private final ChaChaEngine encryptCipher;
+ private final ChaCha20Poly1305 cipher;
+ private final byte[] key;
+ private final byte[] nonce;
public ChachaEncoder(byte[] key, byte[] nonce) throws IOException {
+ this.key = key;
+ // ChaCha20-Poly1305 requires exactly 12 bytes (96 bits) for nonce
+ this.nonce = ensureNonceSize(nonce);
+ this.cipher = new ChaCha20Poly1305();
+ }
+
+ private byte[] ensureNonceSize(byte[] nonce) {
+ if (nonce == null) {
+ return new byte[12]; // Default to zero nonce if null
+ }
+
+ // For HomeKit pairing messages, handle Apple's string-based nonces
+ if (nonce.length == 8) {
+ // Apple's HomeKit implementation uses a specific nonce format
+ // Based on RFC 7539 and Apple's implementation, the nonce should be:
+ // - 4 bytes constant (0x00000000)
+ // - 8 bytes nonce string
+ // This matches ChaCha20's 96-bit nonce requirement and ChachaDecoder format
+ byte[] adjustedNonce = new byte[12];
+ // Put the 8-byte nonce at the END (bytes 4-11), not at the beginning
+ System.arraycopy(nonce, 0, adjustedNonce, 4, 8);
+ // First 4 bytes remain zero (counter initialization)
+ return adjustedNonce;
+ }
+
+ if (nonce.length == 12) {
+ return nonce; // Already correct size
+ }
- this.encryptCipher = new ChaChaEngine(20);
+ byte[] adjustedNonce = new byte[12];
+ if (nonce.length < 12) {
+ // Pad with zeros if too short
+ System.arraycopy(nonce, 0, adjustedNonce, 0, nonce.length);
+ } else {
+ // Truncate if too long
+ System.arraycopy(nonce, 0, adjustedNonce, 0, 12);
+ }
+ return adjustedNonce;
+ }
- this.encryptCipher.init(true, new ParametersWithIV(new KeyParameter(key), nonce));
+ private static String bytesToHex(byte[] bytes) {
+ StringBuilder result = new StringBuilder();
+ for (byte b : bytes) {
+ result.append(String.format("%02x", b));
+ }
+ return result.toString();
}
public byte[] encodeCiphertext(byte[] plaintext) throws IOException {
@@ -22,27 +66,28 @@ public byte[] encodeCiphertext(byte[] plaintext) throws IOException {
}
public byte[] encodeCiphertext(byte[] plaintext, byte[] additionalData) throws IOException {
- KeyParameter macKey = initRecordMAC(encryptCipher);
-
- byte[] ciphertext = new byte[plaintext.length];
- encryptCipher.processBytes(plaintext, 0, plaintext.length, ciphertext, 0);
+ try {
+ // Use the nonce provided during construction
+ AEADParameters params = new AEADParameters(new KeyParameter(key), 128, nonce, additionalData);
+ cipher.init(true, params);
- byte[] calculatedMAC = PolyKeyCreator.create(macKey, additionalData, ciphertext);
-
- byte[] ret = new byte[ciphertext.length + 16];
- System.arraycopy(ciphertext, 0, ret, 0, ciphertext.length);
- System.arraycopy(calculatedMAC, 0, ret, ciphertext.length, 16);
- return ret;
- }
+ byte[] output = new byte[cipher.getOutputSize(plaintext.length)];
+ int len = cipher.processBytes(plaintext, 0, plaintext.length, output, 0);
+ len += cipher.doFinal(output, len);
- private KeyParameter initRecordMAC(ChaChaEngine cipher) {
- byte[] firstBlock = new byte[64];
- cipher.processBytes(firstBlock, 0, firstBlock.length, firstBlock, 0);
+ // Split the result into ciphertext and MAC
+ byte[] ciphertext = new byte[plaintext.length];
+ byte[] mac = new byte[16];
+ System.arraycopy(output, 0, ciphertext, 0, plaintext.length);
+ System.arraycopy(output, plaintext.length, mac, 0, 16);
- // NOTE: The BC implementation puts 'r' after 'k'
- System.arraycopy(firstBlock, 0, firstBlock, 32, 16);
- KeyParameter macKey = new KeyParameter(firstBlock, 16, 32);
- Poly1305KeyGenerator.clamp(macKey.getKey());
- return macKey;
+ // Return combined ciphertext + MAC as expected by the original interface
+ byte[] ret = new byte[ciphertext.length + 16];
+ System.arraycopy(ciphertext, 0, ret, 0, ciphertext.length);
+ System.arraycopy(mac, 0, ret, ciphertext.length, 16);
+ return ret;
+ } catch (InvalidCipherTextException e) {
+ throw new IOException("Encryption failed", e);
+ }
}
}
diff --git a/src/main/java/io/github/hapjava/server/impl/http/HomekitClientConnection.java b/src/main/java/io/github/hapjava/server/impl/http/HomekitClientConnection.java
index 58ef23c6e..2648808bb 100644
--- a/src/main/java/io/github/hapjava/server/impl/http/HomekitClientConnection.java
+++ b/src/main/java/io/github/hapjava/server/impl/http/HomekitClientConnection.java
@@ -13,4 +13,8 @@ public interface HomekitClientConnection {
void close();
void outOfBand(HttpResponse message);
+
+ default String getUsername() {
+ return null;
+ }
}
diff --git a/src/main/java/io/github/hapjava/server/impl/http/impl/AccessoryHandler.java b/src/main/java/io/github/hapjava/server/impl/http/impl/AccessoryHandler.java
index 0e1bf1146..6b840c713 100644
--- a/src/main/java/io/github/hapjava/server/impl/http/impl/AccessoryHandler.java
+++ b/src/main/java/io/github/hapjava/server/impl/http/impl/AccessoryHandler.java
@@ -72,9 +72,9 @@ private void sendResponse(
HttpVersion.HTTP_1_1,
status,
Unpooled.copiedBuffer(responseBody.getBytes(StandardCharsets.UTF_8)));
- response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/plain");
- response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, response.content().readableBytes());
- response.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
+ response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
+ response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
+ response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
ctx.write(response);
ctx.flush();
}
diff --git a/src/main/java/io/github/hapjava/server/impl/http/impl/DefaultHttpRequestImpl.java b/src/main/java/io/github/hapjava/server/impl/http/impl/DefaultHttpRequestImpl.java
index 48a3ceb35..a124705f1 100644
--- a/src/main/java/io/github/hapjava/server/impl/http/impl/DefaultHttpRequestImpl.java
+++ b/src/main/java/io/github/hapjava/server/impl/http/impl/DefaultHttpRequestImpl.java
@@ -13,7 +13,7 @@ public DefaultHttpRequestImpl(io.netty.handler.codec.http.HttpRequest request) {
@Override
public String getUri() {
- return request.getUri();
+ return request.uri();
}
@Override
@@ -23,7 +23,7 @@ public byte[] getBody() {
@Override
public HttpMethod getMethod() {
- io.netty.handler.codec.http.HttpMethod method = request.getMethod();
+ io.netty.handler.codec.http.HttpMethod method = request.method();
if (method.equals(io.netty.handler.codec.http.HttpMethod.GET)) {
return HttpMethod.GET;
} else if (method.equals(io.netty.handler.codec.http.HttpMethod.POST)) {
diff --git a/src/main/java/io/github/hapjava/server/impl/http/impl/NettyResponseUtil.java b/src/main/java/io/github/hapjava/server/impl/http/impl/NettyResponseUtil.java
index fba52eef5..9e7963fb3 100644
--- a/src/main/java/io/github/hapjava/server/impl/http/impl/NettyResponseUtil.java
+++ b/src/main/java/io/github/hapjava/server/impl/http/impl/NettyResponseUtil.java
@@ -4,7 +4,8 @@
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpResponse;
-import io.netty.handler.codec.http.HttpHeaders;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import java.util.Map.Entry;
@@ -25,8 +26,8 @@ public static FullHttpResponse createResponse(HttpResponse homekitResponse) {
for (Entry header : homekitResponse.getHeaders().entrySet()) {
response.headers().add(header.getKey(), header.getValue());
}
- response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, response.content().readableBytes());
- response.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
+ response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
+ response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
return response;
}
}
diff --git a/src/main/java/io/github/hapjava/server/impl/jmdns/JmdnsHomekitAdvertiser.java b/src/main/java/io/github/hapjava/server/impl/jmdns/JmdnsHomekitAdvertiser.java
index 04016561e..1073f9b86 100644
--- a/src/main/java/io/github/hapjava/server/impl/jmdns/JmdnsHomekitAdvertiser.java
+++ b/src/main/java/io/github/hapjava/server/impl/jmdns/JmdnsHomekitAdvertiser.java
@@ -17,6 +17,8 @@ public class JmdnsHomekitAdvertiser {
private static final String SERVICE_TYPE = "_hap._tcp.local.";
private final JmDNS jmdns;
+ private final boolean ownsJmdns;
+ private Thread shutdownHook;
private boolean discoverable = true;
private static final Logger logger = LoggerFactory.getLogger(JmdnsHomekitAdvertiser.class);
private boolean isAdvertising = false;
@@ -27,17 +29,24 @@ public class JmdnsHomekitAdvertiser {
private int port;
private int configurationIndex;
private ServiceInfo serviceInfo;
+ private int category;
public JmdnsHomekitAdvertiser(JmDNS jmdns) {
- this.jmdns = jmdns;
+ this(jmdns, false);
}
public JmdnsHomekitAdvertiser(InetAddress localAddress) throws UnknownHostException, IOException {
- jmdns = JmDNS.create(localAddress);
+ this(JmDNS.create(localAddress), true);
+ }
+
+ JmdnsHomekitAdvertiser(JmDNS jmdns, boolean ownsJmdns) {
+ this.jmdns = jmdns;
+ this.ownsJmdns = ownsJmdns;
}
public synchronized void advertise(
- String label, String mac, int port, int configurationIndex, String setupId) throws Exception {
+ String label, int category, String mac, int port, int configurationIndex, String setupId)
+ throws Exception {
if (isAdvertising) {
throw new IllegalStateException("HomeKit advertiser is already running");
}
@@ -45,24 +54,40 @@ public synchronized void advertise(
this.mac = mac;
this.port = port;
this.setupId = setupId;
+ this.category = category;
this.configurationIndex = configurationIndex;
logger.trace("Advertising accessory " + label);
registerService();
- Runtime.getRuntime()
- .addShutdownHook(
- new Thread(
- () -> {
- logger.trace("Stopping advertising in response to shutdown.");
- jmdns.unregisterAllServices();
- }));
+ shutdownHook =
+ new Thread(
+ () -> {
+ logger.trace("Stopping advertising in response to shutdown.");
+ jmdns.unregisterAllServices();
+ });
+ Runtime.getRuntime().addShutdownHook(shutdownHook);
isAdvertising = true;
}
public synchronized void stop() {
unregisterService();
+ isAdvertising = false;
+ if (shutdownHook != null) {
+ try {
+ Runtime.getRuntime().removeShutdownHook(shutdownHook);
+ } catch (IllegalStateException e) {
+ // already shutting down
+ }
+ }
+ if (ownsJmdns) {
+ try {
+ jmdns.close();
+ } catch (IOException e) {
+ logger.warn("Failed to close mDNS", e);
+ }
+ }
}
public synchronized void setDiscoverable(boolean discoverable) throws IOException {
@@ -76,6 +101,17 @@ public synchronized void setDiscoverable(boolean discoverable) throws IOExceptio
}
}
+ public synchronized void setMac(String mac) throws IOException {
+ if (this.mac != mac) {
+ this.mac = mac;
+ if (isAdvertising) {
+ logger.trace("Re-creating service due to change in mac to " + mac);
+ unregisterService();
+ registerService();
+ }
+ }
+ }
+
public synchronized void setConfigurationIndex(int revision) throws IOException {
if (this.configurationIndex != revision) {
this.configurationIndex = revision;
@@ -114,7 +150,9 @@ private ServiceInfo buildServiceInfo() {
props.put("c#", Integer.toString(configurationIndex));
props.put("s#", "1");
props.put("ff", "0");
- props.put("ci", "1");
+ props.put("ci", Integer.toString(category));
+ props.put("pv", "1.1");
+
return ServiceInfo.create(SERVICE_TYPE, label, port, 1, 1, props);
}
}
diff --git a/src/main/java/io/github/hapjava/server/impl/json/AccessoryController.java b/src/main/java/io/github/hapjava/server/impl/json/AccessoryController.java
index 98e6cd266..fdd223ec3 100644
--- a/src/main/java/io/github/hapjava/server/impl/json/AccessoryController.java
+++ b/src/main/java/io/github/hapjava/server/impl/json/AccessoryController.java
@@ -18,6 +18,7 @@
import javax.json.JsonArrayBuilder;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
+import javax.json.JsonWriter;
public class AccessoryController {
@@ -41,7 +42,7 @@ public HttpResponse listing() throws Exception {
iidLookup.putAll(swapKeyAndValue(registry.getCharacteristics(accessory.getId())));
for (Service service : servicesByInterfaceId.values()) {
- serviceFutures.add(toJson(service, iidLookup));
+ serviceFutures.add(toJson(service, iidLookup, accessory.getPrimaryService() == service));
}
accessoryServiceFutures.put(accessory.getId(), serviceFutures);
@@ -64,15 +65,15 @@ public HttpResponse listing() throws Exception {
.add("services", serviceArrayBuilders.get(accessory.getId())));
}
- try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
- Json.createWriter(baos)
- .write(Json.createObjectBuilder().add("accessories", accessories).build());
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ JsonWriter jsonWriter = Json.createWriter(baos)) {
+ jsonWriter.write(Json.createObjectBuilder().add("accessories", accessories).build());
return new HapJsonResponse(baos.toByteArray());
}
}
- private CompletableFuture toJson(Service service, Map