Skip to content

Commit de6822d

Browse files
committed
Advertise paired status to fix iOS9
iOS9 requires that homekit accessories advertise their paired status. It seems like Apple is enforcing a single user being paired with an accessory. However, it's not entirely clear, so the fix does not involve refactoring HomekitAuthInfo around a single user. Instead, HomekitAuthInfo now includes a hasUser method that should be implemented.
1 parent 569a004 commit de6822d

File tree

9 files changed

+81
-25
lines changed

9 files changed

+81
-25
lines changed

src/main/java/com/beowulfe/hap/HomekitAuthInfo.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,13 @@ public interface HomekitAuthInfo {
6969
* @return the previously stored public key for this user.
7070
*/
7171
byte[] getUserPublicKey(String username);
72+
73+
/**
74+
* Called to check if a user has been created. The homekit accessory advertises whether the accessory has already been paired.
75+
* At this time, it's unclear whether multiple users can be created, however it is known that advertising as unpaired
76+
* will break in iOS 9. The default value has been provided to maintain API compatibility for implementations targeting iOS 8.
77+
*
78+
* @return whether a user has been created and stored
79+
*/
80+
default boolean hasUser() { return false; };
7281
}

src/main/java/com/beowulfe/hap/HomekitRoot.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,9 @@ public void start() {
9898
started = true;
9999
registry.reset();
100100
webHandler.start(new HomekitClientConnectionFactoryImpl(authInfo,
101-
registry, subscriptions)).thenAccept(port -> {
101+
registry, subscriptions, advertiser)).thenAccept(port -> {
102102
try {
103+
advertiser.setDiscoverable(authInfo.hasUser());
103104
advertiser.advertise(label, authInfo.getMac(), port);
104105
} catch (Exception e) {
105106
throw new RuntimeException(e);

src/main/java/com/beowulfe/hap/impl/connections/ConnectionImpl.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.beowulfe.hap.impl.http.HomekitClientConnection;
1919
import com.beowulfe.hap.impl.http.HttpRequest;
2020
import com.beowulfe.hap.impl.http.HttpResponse;
21+
import com.beowulfe.hap.impl.jmdns.JmdnsHomekitAdvertiser;
2122
import com.beowulfe.hap.impl.pairing.UpgradeResponse;
2223

2324
class ConnectionImpl implements HomekitClientConnection {
@@ -35,8 +36,9 @@ class ConnectionImpl implements HomekitClientConnection {
3536
private final static Logger LOGGER = LoggerFactory.getLogger(HomekitClientConnection.class);
3637

3738
public ConnectionImpl(HomekitAuthInfo authInfo, HomekitRegistry registry,
38-
Consumer<HttpResponse> outOfBandMessageCallback, SubscriptionManager subscriptions) {
39-
httpSession = new HttpSession(authInfo, registry, subscriptions, this);
39+
Consumer<HttpResponse> outOfBandMessageCallback, SubscriptionManager subscriptions,
40+
JmdnsHomekitAdvertiser advertiser) {
41+
httpSession = new HttpSession(authInfo, registry, subscriptions, this, advertiser);
4042
this.outOfBandMessageCallback = outOfBandMessageCallback;
4143
this.subscriptions = subscriptions;
4244
}

src/main/java/com/beowulfe/hap/impl/connections/HomekitClientConnectionFactoryImpl.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,26 @@
77
import com.beowulfe.hap.impl.http.HomekitClientConnection;
88
import com.beowulfe.hap.impl.http.HomekitClientConnectionFactory;
99
import com.beowulfe.hap.impl.http.HttpResponse;
10+
import com.beowulfe.hap.impl.jmdns.JmdnsHomekitAdvertiser;
1011

1112
public class HomekitClientConnectionFactoryImpl implements HomekitClientConnectionFactory{
1213

1314
private final HomekitAuthInfo authInfo;
1415
private final HomekitRegistry registry;
1516
private final SubscriptionManager subscriptions;
17+
private final JmdnsHomekitAdvertiser advertiser;
1618

1719
public HomekitClientConnectionFactoryImpl(HomekitAuthInfo authInfo,
18-
HomekitRegistry registry, SubscriptionManager subscriptions) {
20+
HomekitRegistry registry, SubscriptionManager subscriptions, JmdnsHomekitAdvertiser advertiser) {
1921
this.registry = registry;
2022
this.authInfo = authInfo;
2123
this.subscriptions = subscriptions;
24+
this.advertiser = advertiser;
2225
}
2326

2427
@Override
2528
public HomekitClientConnection createConnection(Consumer<HttpResponse> outOfBandMessageCallback) {
26-
return new ConnectionImpl(authInfo, registry, outOfBandMessageCallback, subscriptions);
29+
return new ConnectionImpl(authInfo, registry, outOfBandMessageCallback, subscriptions, advertiser);
2730
}
2831

2932

src/main/java/com/beowulfe/hap/impl/connections/HttpSession.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import com.beowulfe.hap.impl.http.HomekitClientConnection;
1313
import com.beowulfe.hap.impl.http.HttpRequest;
1414
import com.beowulfe.hap.impl.http.HttpResponse;
15+
import com.beowulfe.hap.impl.jmdns.JmdnsHomekitAdvertiser;
1516
import com.beowulfe.hap.impl.json.AccessoryController;
1617
import com.beowulfe.hap.impl.json.CharacteristicsController;
1718
import com.beowulfe.hap.impl.pairing.PairVerificationManager;
@@ -31,15 +32,17 @@ class HttpSession {
3132
private final HomekitRegistry registry;
3233
private final SubscriptionManager subscriptions;
3334
private final HomekitClientConnection connection;
35+
private final JmdnsHomekitAdvertiser advertiser;
3436

3537
private final static Logger logger = LoggerFactory.getLogger(HttpSession.class);
3638

3739
public HttpSession(HomekitAuthInfo authInfo, HomekitRegistry registry, SubscriptionManager subscriptions,
38-
HomekitClientConnection connection) {
40+
HomekitClientConnection connection, JmdnsHomekitAdvertiser advertiser) {
3941
this.authInfo = authInfo;
4042
this.registry = registry;
4143
this.subscriptions = subscriptions;
4244
this.connection = connection;
45+
this.advertiser = advertiser;
4346
}
4447

4548
public HttpResponse handleRequest(HttpRequest request) throws IOException {
@@ -76,7 +79,7 @@ public HttpResponse handleAuthenticatedRequest(HttpRequest request) throws IOExc
7679
}
7780

7881
case "/pairings":
79-
return new PairingUpdateController(authInfo).handle(request);
82+
return new PairingUpdateController(authInfo, advertiser).handle(request);
8083

8184
default:
8285
if (request.getUri().startsWith("/characteristics?")) {
@@ -95,7 +98,7 @@ private HttpResponse handlePairSetup(HttpRequest request) {
9598
if (pairingManager == null) {
9699
synchronized(HttpSession.class) {
97100
if (pairingManager == null) {
98-
pairingManager = new PairingManager(authInfo, registry);
101+
pairingManager = new PairingManager(authInfo, registry, advertiser);
99102
}
100103
}
101104
}

src/main/java/com/beowulfe/hap/impl/jmdns/JmdnsHomekitAdvertiser.java

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,35 +17,61 @@ public class JmdnsHomekitAdvertiser {
1717
private static final String SERVICE_TYPE = "_hap._tcp.local.";
1818

1919
private final JmDNS jmdns;
20-
20+
private boolean discoverable = true;
2121
private final static Logger logger = LoggerFactory.getLogger(JmdnsHomekitAdvertiser.class);
22+
private boolean isAdvertising = false;
23+
24+
private String label;
25+
private String mac;
26+
private int port;
2227

2328
public JmdnsHomekitAdvertiser(InetAddress localAddress) throws UnknownHostException, IOException {
2429
jmdns = JmDNS.create(localAddress);
2530
}
2631

27-
public void advertise(String label, String mac, int port) throws Exception {
32+
public synchronized void advertise(String label, String mac, int port) throws Exception {
33+
if (isAdvertising) {
34+
throw new IllegalStateException("Homekit advertiser is already running");
35+
}
36+
this.label = label;
37+
this.mac = mac;
38+
this.port = port;
39+
2840
logger.info("Advertising accessory "+label);
2941

30-
logger.info("Registering "+SERVICE_TYPE+" on port "+port);
31-
Map<String, String> props = new HashMap<>();
32-
props.put("sf", "1");
33-
props.put("pv", "1.0");
34-
props.put("id", mac);
35-
props.put("md", label);
36-
props.put("c#", "1");
37-
props.put("s#", "1");
38-
props.put("ff", "0");
39-
jmdns.registerService(ServiceInfo.create(SERVICE_TYPE, label, port, 1, 1, props));
42+
registerService();
4043

4144
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
4245
logger.info("Stopping advertising in response to shutdown.");
4346
jmdns.unregisterAllServices();
4447
}));
48+
isAdvertising = true;
4549
}
4650

4751
public void stop() {
4852
jmdns.unregisterAllServices();
4953
}
5054

55+
public void setDiscoverable(boolean discoverable) throws IOException {
56+
this.discoverable = discoverable;
57+
if (isAdvertising) {
58+
logger.info("Re-creating service due to change in discoverability to "+discoverable);
59+
jmdns.unregisterAllServices();
60+
registerService();
61+
}
62+
}
63+
64+
private void registerService() throws IOException {
65+
logger.info("Registering "+SERVICE_TYPE+" on port "+port);
66+
Map<String, String> props = new HashMap<>();
67+
props.put("sf", discoverable ? "1" : "0");
68+
props.put("pv", "1.0");
69+
props.put("id", mac);
70+
props.put("md", label);
71+
props.put("c#", "1");
72+
props.put("s#", "1");
73+
props.put("ff", "0");
74+
jmdns.registerService(ServiceInfo.create(SERVICE_TYPE, label, port, 1, 1, props));
75+
}
76+
5177
}

src/main/java/com/beowulfe/hap/impl/pairing/FinalPairHandler.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.beowulfe.hap.impl.crypto.EdsaSigner;
1111
import com.beowulfe.hap.impl.crypto.EdsaVerifier;
1212
import com.beowulfe.hap.impl.http.HttpResponse;
13+
import com.beowulfe.hap.impl.jmdns.JmdnsHomekitAdvertiser;
1314
import com.beowulfe.hap.impl.pairing.PairSetupRequest.Stage3Request;
1415
import com.beowulfe.hap.impl.pairing.TypeLengthValueUtils.DecodeResult;
1516
import com.beowulfe.hap.impl.pairing.TypeLengthValueUtils.Encoder;
@@ -18,12 +19,14 @@ class FinalPairHandler {
1819

1920
private final byte[] k;
2021
private final HomekitAuthInfo authInfo;
22+
private final JmdnsHomekitAdvertiser advertiser;
2123

2224
private byte[] hkdf_enc_key;
2325

24-
public FinalPairHandler(byte[] k, HomekitAuthInfo authInfo) {
26+
public FinalPairHandler(byte[] k, HomekitAuthInfo authInfo, JmdnsHomekitAdvertiser advertiser) {
2527
this.k = k;
2628
this.authInfo = authInfo;
29+
this.advertiser = advertiser;
2730
}
2831

2932
public HttpResponse handle(PairSetupRequest req) throws Exception {
@@ -60,7 +63,7 @@ private HttpResponse createUser(byte[] username, byte[] ltpk, byte[] proof) thro
6063
throw new Exception("Invalid signature");
6164
}
6265
authInfo.createUser(authInfo.getMac()+new String(username), ltpk);
63-
66+
advertiser.setDiscoverable(false);
6467
return createResponse();
6568
}
6669

src/main/java/com/beowulfe/hap/impl/pairing/PairingManager.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.beowulfe.hap.impl.HomekitRegistry;
88
import com.beowulfe.hap.impl.http.HttpRequest;
99
import com.beowulfe.hap.impl.http.HttpResponse;
10+
import com.beowulfe.hap.impl.jmdns.JmdnsHomekitAdvertiser;
1011
import com.beowulfe.hap.impl.responses.NotFoundResponse;
1112
import com.beowulfe.hap.impl.responses.UnauthorizedResponse;
1213

@@ -16,12 +17,14 @@ public class PairingManager {
1617

1718
private final HomekitAuthInfo authInfo;
1819
private final HomekitRegistry registry;
20+
private final JmdnsHomekitAdvertiser advertiser;
1921

2022
private SrpHandler srpHandler;
2123

22-
public PairingManager(HomekitAuthInfo authInfo, HomekitRegistry registry) {
24+
public PairingManager(HomekitAuthInfo authInfo, HomekitRegistry registry, JmdnsHomekitAdvertiser advertiser) {
2325
this.authInfo = authInfo;
2426
this.registry = registry;
27+
this.advertiser = advertiser;
2528
}
2629

2730
public HttpResponse handle(HttpRequest httpRequest) throws Exception {
@@ -51,7 +54,7 @@ public HttpResponse handle(HttpRequest httpRequest) throws Exception {
5154
logger.warn("Received unexpected stage 3 request for "+registry.getLabel());
5255
return new UnauthorizedResponse();
5356
} else {
54-
FinalPairHandler handler = new FinalPairHandler(srpHandler.getK(), authInfo);
57+
FinalPairHandler handler = new FinalPairHandler(srpHandler.getK(), authInfo, advertiser);
5558
try {
5659
return handler.handle(req);
5760
} catch (Exception e) {

src/main/java/com/beowulfe/hap/impl/pairing/PairingUpdateController.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@
55
import com.beowulfe.hap.HomekitAuthInfo;
66
import com.beowulfe.hap.impl.http.HttpRequest;
77
import com.beowulfe.hap.impl.http.HttpResponse;
8+
import com.beowulfe.hap.impl.jmdns.JmdnsHomekitAdvertiser;
89
import com.beowulfe.hap.impl.pairing.TypeLengthValueUtils.DecodeResult;
910

1011
public class PairingUpdateController {
1112

1213
private final HomekitAuthInfo authInfo;
14+
private final JmdnsHomekitAdvertiser advertiser;
1315

14-
public PairingUpdateController(HomekitAuthInfo authInfo) {
16+
public PairingUpdateController(HomekitAuthInfo authInfo, JmdnsHomekitAdvertiser advertiser) {
1517
this.authInfo = authInfo;
18+
this.advertiser = advertiser;
1619
}
1720

1821
public HttpResponse handle(HttpRequest request) throws IOException {
@@ -26,6 +29,9 @@ public HttpResponse handle(HttpRequest request) throws IOException {
2629
} else if (method == 4) { //Remove pairing
2730
byte[] username = d.getBytes(MessageType.USERNAME);
2831
authInfo.removeUser(authInfo.getMac()+new String(username));
32+
if (!authInfo.hasUser()) {
33+
advertiser.setDiscoverable(false);
34+
}
2935
} else {
3036
throw new RuntimeException("Unrecognized method: "+method);
3137
}

0 commit comments

Comments
 (0)