UNPKG

51.3 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.HAPServer = exports.HAPServerEventTypes = exports.HAPPairingHTTPCode = exports.HAPHTTPCode = exports.HAPStatus = exports.TLVErrorCode = void 0;
4exports.IsKnownHAPStatusError = IsKnownHAPStatusError;
5const tslib_1 = require("tslib");
6const crypto_1 = tslib_1.__importDefault(require("crypto"));
7const debug_1 = tslib_1.__importDefault(require("debug"));
8const events_1 = require("events");
9const fast_srp_hap_1 = require("fast-srp-hap");
10const tweetnacl_1 = tslib_1.__importDefault(require("tweetnacl"));
11const url_1 = require("url");
12const internal_types_1 = require("../internal-types");
13const eventedhttp_1 = require("./util/eventedhttp");
14const hapCrypto = tslib_1.__importStar(require("./util/hapCrypto"));
15const once_1 = require("./util/once");
16const tlv = tslib_1.__importStar(require("./util/tlv"));
17const debug = (0, debug_1.default)("HAP-NodeJS:HAPServer");
18/**
19 * TLV error codes for the `TLVValues.ERROR_CODE` field.
20 *
21 * @group HAP Accessory Server
22 */
23var TLVErrorCode;
24(function (TLVErrorCode) {
25 // noinspection JSUnusedGlobalSymbols
26 TLVErrorCode[TLVErrorCode["UNKNOWN"] = 1] = "UNKNOWN";
27 TLVErrorCode[TLVErrorCode["INVALID_REQUEST"] = 2] = "INVALID_REQUEST";
28 // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
29 TLVErrorCode[TLVErrorCode["AUTHENTICATION"] = 2] = "AUTHENTICATION";
30 TLVErrorCode[TLVErrorCode["BACKOFF"] = 3] = "BACKOFF";
31 TLVErrorCode[TLVErrorCode["MAX_PEERS"] = 4] = "MAX_PEERS";
32 TLVErrorCode[TLVErrorCode["MAX_TRIES"] = 5] = "MAX_TRIES";
33 TLVErrorCode[TLVErrorCode["UNAVAILABLE"] = 6] = "UNAVAILABLE";
34 TLVErrorCode[TLVErrorCode["BUSY"] = 7] = "BUSY"; // cannot accept pairing request at this time
35})(TLVErrorCode || (exports.TLVErrorCode = TLVErrorCode = {}));
36/**
37 * @group HAP Accessory Server
38 */
39var HAPStatus;
40(function (HAPStatus) {
41 // noinspection JSUnusedGlobalSymbols
42 /**
43 * Success of the request.
44 */
45 HAPStatus[HAPStatus["SUCCESS"] = 0] = "SUCCESS";
46 /**
47 * The request was rejected due to insufficient privileges.
48 */
49 HAPStatus[HAPStatus["INSUFFICIENT_PRIVILEGES"] = -70401] = "INSUFFICIENT_PRIVILEGES";
50 /**
51 * Operation failed due to some communication failure with the characteristic.
52 */
53 HAPStatus[HAPStatus["SERVICE_COMMUNICATION_FAILURE"] = -70402] = "SERVICE_COMMUNICATION_FAILURE";
54 /**
55 * The resource is busy. Try again.
56 */
57 HAPStatus[HAPStatus["RESOURCE_BUSY"] = -70403] = "RESOURCE_BUSY";
58 /**
59 * Cannot write a read-only characteristic ({@link Perms.PAIRED_WRITE} not defined).
60 */
61 HAPStatus[HAPStatus["READ_ONLY_CHARACTERISTIC"] = -70404] = "READ_ONLY_CHARACTERISTIC";
62 /**
63 * Cannot read from a write-only characteristic ({@link Perms.PAIRED_READ} not defined).
64 */
65 HAPStatus[HAPStatus["WRITE_ONLY_CHARACTERISTIC"] = -70405] = "WRITE_ONLY_CHARACTERISTIC";
66 /**
67 * Event notifications are not supported for the requested characteristic ({@link Perms.NOTIFY} not defined).
68 */
69 HAPStatus[HAPStatus["NOTIFICATION_NOT_SUPPORTED"] = -70406] = "NOTIFICATION_NOT_SUPPORTED";
70 /**
71 * The device is out of resources to process the request.
72 */
73 HAPStatus[HAPStatus["OUT_OF_RESOURCE"] = -70407] = "OUT_OF_RESOURCE";
74 /**
75 * The operation timed out.
76 */
77 HAPStatus[HAPStatus["OPERATION_TIMED_OUT"] = -70408] = "OPERATION_TIMED_OUT";
78 /**
79 * The given resource does not exist.
80 */
81 HAPStatus[HAPStatus["RESOURCE_DOES_NOT_EXIST"] = -70409] = "RESOURCE_DOES_NOT_EXIST";
82 /**
83 * Received an invalid value in the given request for the given characteristic.
84 */
85 HAPStatus[HAPStatus["INVALID_VALUE_IN_REQUEST"] = -70410] = "INVALID_VALUE_IN_REQUEST";
86 /**
87 * Insufficient authorization.
88 */
89 HAPStatus[HAPStatus["INSUFFICIENT_AUTHORIZATION"] = -70411] = "INSUFFICIENT_AUTHORIZATION";
90 /**
91 * Operation not allowed in the current state.
92 */
93 HAPStatus[HAPStatus["NOT_ALLOWED_IN_CURRENT_STATE"] = -70412] = "NOT_ALLOWED_IN_CURRENT_STATE";
94 // when adding new status codes, remember to update bounds in IsKnownHAPStatusError below
95})(HAPStatus || (exports.HAPStatus = HAPStatus = {}));
96/**
97 * Determines if the given status code is a known {@link HAPStatus} error code.
98 *
99 * @group HAP Accessory Server
100 */
101function IsKnownHAPStatusError(status) {
102 return (
103 // Lower bound (most negative error code)
104 status >= -70412 /* HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE */ &&
105 // Upper bound (negative error code closest to zero)
106 status <= -70401 /* HAPStatus.INSUFFICIENT_PRIVILEGES */);
107}
108/**
109 * Those status codes are the one listed as appropriate for the HAP spec!
110 *
111 * When the response is a client error 4xx or server error 5xx, the response
112 * must include a status {@link HAPStatus} property.
113 *
114 * When the response is a MULTI_STATUS EVERY entry in the characteristics property MUST include a status property (even success).
115 *
116 * @group HAP Accessory Server
117 */
118var HAPHTTPCode;
119(function (HAPHTTPCode) {
120 // noinspection JSUnusedGlobalSymbols
121 HAPHTTPCode[HAPHTTPCode["OK"] = 200] = "OK";
122 HAPHTTPCode[HAPHTTPCode["NO_CONTENT"] = 204] = "NO_CONTENT";
123 HAPHTTPCode[HAPHTTPCode["MULTI_STATUS"] = 207] = "MULTI_STATUS";
124 // client error
125 HAPHTTPCode[HAPHTTPCode["BAD_REQUEST"] = 400] = "BAD_REQUEST";
126 HAPHTTPCode[HAPHTTPCode["NOT_FOUND"] = 404] = "NOT_FOUND";
127 HAPHTTPCode[HAPHTTPCode["UNPROCESSABLE_ENTITY"] = 422] = "UNPROCESSABLE_ENTITY";
128 // server error
129 HAPHTTPCode[HAPHTTPCode["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR";
130 HAPHTTPCode[HAPHTTPCode["SERVICE_UNAVAILABLE"] = 503] = "SERVICE_UNAVAILABLE";
131})(HAPHTTPCode || (exports.HAPHTTPCode = HAPHTTPCode = {}));
132/**
133 * When in a request is made to the pairing endpoints, and mime type is 'application/pairing+tlv8'
134 * one should use the below status codes.
135 *
136 * @group HAP Accessory Server
137 */
138var HAPPairingHTTPCode;
139(function (HAPPairingHTTPCode) {
140 // noinspection JSUnusedGlobalSymbols
141 HAPPairingHTTPCode[HAPPairingHTTPCode["OK"] = 200] = "OK";
142 HAPPairingHTTPCode[HAPPairingHTTPCode["BAD_REQUEST"] = 400] = "BAD_REQUEST";
143 HAPPairingHTTPCode[HAPPairingHTTPCode["METHOD_NOT_ALLOWED"] = 405] = "METHOD_NOT_ALLOWED";
144 HAPPairingHTTPCode[HAPPairingHTTPCode["TOO_MANY_REQUESTS"] = 429] = "TOO_MANY_REQUESTS";
145 HAPPairingHTTPCode[HAPPairingHTTPCode["CONNECTION_AUTHORIZATION_REQUIRED"] = 470] = "CONNECTION_AUTHORIZATION_REQUIRED";
146 HAPPairingHTTPCode[HAPPairingHTTPCode["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR";
147})(HAPPairingHTTPCode || (exports.HAPPairingHTTPCode = HAPPairingHTTPCode = {}));
148/**
149 * @group HAP Accessory Server
150 */
151var HAPServerEventTypes;
152(function (HAPServerEventTypes) {
153 /**
154 * Emitted when the server is fully set up and ready to receive connections.
155 */
156 HAPServerEventTypes["LISTENING"] = "listening";
157 /**
158 * Emitted when a client wishes for this server to identify itself before pairing. You must call the
159 * callback to respond to the client with success.
160 */
161 HAPServerEventTypes["IDENTIFY"] = "identify";
162 HAPServerEventTypes["ADD_PAIRING"] = "add-pairing";
163 HAPServerEventTypes["REMOVE_PAIRING"] = "remove-pairing";
164 HAPServerEventTypes["LIST_PAIRINGS"] = "list-pairings";
165 /**
166 * This event is emitted when a client completes the "pairing" process and exchanges encryption keys.
167 * Note that this does not mean the "Add Accessory" process in iOS has completed.
168 * You must call the callback to complete the process.
169 */
170 HAPServerEventTypes["PAIR"] = "pair";
171 /**
172 * This event is emitted when a client requests the complete representation of Accessory data for
173 * this Accessory (for instance, what services, characteristics, etc. are supported) and any bridged
174 * Accessories in the case of a Bridge Accessory. The listener must call the provided callback function
175 * when the accessory data is ready. We will automatically JSON.stringify the data.
176 */
177 HAPServerEventTypes["ACCESSORIES"] = "accessories";
178 /**
179 * This event is emitted when a client wishes to retrieve the current value of one or more characteristics.
180 * The listener must call the provided callback function when the values are ready. iOS clients can typically
181 * wait up to 10 seconds for this call to return. We will automatically JSON.stringify the data (which must
182 * be an array) and wrap it in an object with a top-level "characteristics" property.
183 */
184 HAPServerEventTypes["GET_CHARACTERISTICS"] = "get-characteristics";
185 /**
186 * This event is emitted when a client wishes to set the current value of one or more characteristics and/or
187 * subscribe to one or more events. The 'events' param is an initially-empty object, associated with the current
188 * connection, on which you may store event registration keys for later processing. The listener must call
189 * the provided callback when the request has been processed.
190 */
191 HAPServerEventTypes["SET_CHARACTERISTICS"] = "set-characteristics";
192 HAPServerEventTypes["REQUEST_RESOURCE"] = "request-resource";
193 HAPServerEventTypes["CONNECTION_CLOSED"] = "connection-closed";
194})(HAPServerEventTypes || (exports.HAPServerEventTypes = HAPServerEventTypes = {}));
195/**
196 * The actual HAP server that iOS devices talk to.
197 *
198 * Notes
199 * -----
200 * It turns out that the IP-based version of HomeKit's HAP protocol operates over a sort of pseudo-HTTP.
201 * Accessories are meant to host a TCP socket server that initially behaves exactly as an HTTP/1.1 server.
202 * So iOS devices will open up a long-lived connection to this server and begin issuing HTTP requests.
203 * So far, this conforms with HTTP/1.1 Keepalive. However, after the "pairing" process is complete, the
204 * connection is expected to be "upgraded" to support full-packet encryption of both HTTP headers and data.
205 * This encryption is NOT SSL. It is a customized ChaCha20+Poly1305 encryption layer.
206 *
207 * Additionally, this "HTTP Server" supports sending "event" responses at any time without warning. The iOS
208 * device simply keeps the connection open after it's finished with HTTP request/response traffic, and while
209 * the connection is open, the server can elect to issue "EVENT/1.0 200 OK" HTTP-style responses. These are
210 * typically sent to inform the iOS device of a characteristic change for the accessory (like "Door was Unlocked").
211 *
212 * See {@link EventedHTTPServer} for more detail on the implementation of this protocol.
213 *
214 * @group HAP Accessory Server
215 */
216// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
217class HAPServer extends events_1.EventEmitter {
218 accessoryInfo;
219 httpServer;
220 unsuccessfulPairAttempts = 0; // after 100 unsuccessful attempts the server won't accept any further attempts. Will currently be reset on a reboot
221 allowInsecureRequest;
222 constructor(accessoryInfo) {
223 super();
224 this.accessoryInfo = accessoryInfo;
225 this.allowInsecureRequest = false;
226 // internal server that does all the actual communication
227 this.httpServer = new eventedhttp_1.EventedHTTPServer();
228 this.httpServer.on("listening" /* EventedHTTPServerEvent.LISTENING */, this.onListening.bind(this));
229 this.httpServer.on("request" /* EventedHTTPServerEvent.REQUEST */, this.handleRequestOnHAPConnection.bind(this));
230 this.httpServer.on("connection-closed" /* EventedHTTPServerEvent.CONNECTION_CLOSED */, this.handleConnectionClosed.bind(this));
231 }
232 listen(port = 0, host) {
233 if (host === "::") {
234 // this will work around "EAFNOSUPPORT: address family not supported" errors
235 // on systems where IPv6 is not supported/enabled, we just use the node default then by supplying undefined
236 host = undefined;
237 }
238 this.httpServer.listen(port, host);
239 }
240 stop() {
241 this.httpServer.stop();
242 }
243 destroy() {
244 this.stop();
245 this.removeAllListeners();
246 }
247 /**
248 * Send an even notification for given characteristic and changed value to all connected clients.
249 * If `originator` is specified, the given {@link HAPConnection} will be excluded from the broadcast.
250 *
251 * @param aid - The accessory id of the updated characteristic.
252 * @param iid - The instance id of the updated characteristic.
253 * @param value - The newly set value of the characteristic.
254 * @param originator - If specified, the connection will not get an event message.
255 * @param immediateDelivery - The HAP spec requires some characteristics to be delivery immediately.
256 * Namely, for the {@link Characteristic.ButtonEvent} and the {@link Characteristic.ProgrammableSwitchEvent} characteristics.
257 */
258 sendEventNotifications(aid, iid, value, originator, immediateDelivery) {
259 try {
260 this.httpServer.broadcastEvent(aid, iid, value, originator, immediateDelivery);
261 }
262 catch (error) {
263 console.warn("[" + this.accessoryInfo.username + "] Error when sending event notifications: " + error.message);
264 }
265 }
266 onListening(port, hostname) {
267 this.emit("listening" /* HAPServerEventTypes.LISTENING */, port, hostname);
268 }
269 // Called when an HTTP request was detected.
270 handleRequestOnHAPConnection(connection, request, response) {
271 debug("[%s] HAP Request: %s %s", this.accessoryInfo.username, request.method, request.url);
272 const buffers = [];
273 request.on("data", data => buffers.push(data));
274 request.on("end", () => {
275 const url = new url_1.URL(request.url, "http://hap-nodejs.local"); // parse the url (query strings etc)
276 const handler = this.getHandler(url);
277 if (!handler) {
278 debug("[%s] WARNING: Handler for %s not implemented", this.accessoryInfo.username, request.url);
279 response.writeHead(404 /* HAPHTTPCode.NOT_FOUND */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ });
280 response.end(JSON.stringify({ status: -70409 /* HAPStatus.RESOURCE_DOES_NOT_EXIST */ }));
281 }
282 else {
283 const data = Buffer.concat(buffers);
284 try {
285 handler(connection, url, request, data, response);
286 }
287 catch (error) {
288 debug("[%s] Error executing route handler: %s", this.accessoryInfo.username, error.stack);
289 response.writeHead(500 /* HAPHTTPCode.INTERNAL_SERVER_ERROR */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ });
290 response.end(JSON.stringify({ status: -70403 /* HAPStatus.RESOURCE_BUSY */ })); // resource busy try again, does somehow fit?
291 }
292 }
293 });
294 }
295 handleConnectionClosed(connection) {
296 this.emit("connection-closed" /* HAPServerEventTypes.CONNECTION_CLOSED */, connection);
297 }
298 getHandler(url) {
299 switch (url.pathname.toLowerCase()) {
300 case "/identify":
301 return this.handleIdentifyRequest.bind(this);
302 case "/pair-setup":
303 return this.handlePairSetup.bind(this);
304 case "/pair-verify":
305 return this.handlePairVerify.bind(this);
306 case "/pairings":
307 return this.handlePairings.bind(this);
308 case "/accessories":
309 return this.handleAccessories.bind(this);
310 case "/characteristics":
311 return this.handleCharacteristics.bind(this);
312 case "/prepare":
313 return this.handlePrepareWrite.bind(this);
314 case "/resource":
315 return this.handleResource.bind(this);
316 default:
317 return undefined;
318 }
319 }
320 /**
321 * UNPAIRED Accessory identification.
322 */
323 handleIdentifyRequest(connection, url, request, data, response) {
324 // POST body is empty
325 if (this.accessoryInfo.paired() && !this.allowInsecureRequest) {
326 response.writeHead(400 /* HAPHTTPCode.BAD_REQUEST */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ });
327 response.end(JSON.stringify({ status: -70401 /* HAPStatus.INSUFFICIENT_PRIVILEGES */ }));
328 return;
329 }
330 this.emit("identify" /* HAPServerEventTypes.IDENTIFY */, (0, once_1.once)(err => {
331 if (!err) {
332 debug("[%s] Identification success", this.accessoryInfo.username);
333 response.writeHead(204 /* HAPHTTPCode.NO_CONTENT */);
334 response.end();
335 }
336 else {
337 debug("[%s] Identification error: %s", this.accessoryInfo.username, err.message);
338 response.writeHead(500 /* HAPHTTPCode.INTERNAL_SERVER_ERROR */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ });
339 response.end(JSON.stringify({ status: -70403 /* HAPStatus.RESOURCE_BUSY */ }));
340 }
341 }));
342 }
343 handlePairSetup(connection, url, request, data, response) {
344 // Can only be directly paired with one iOS device
345 if (!this.allowInsecureRequest && this.accessoryInfo.paired()) {
346 response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" });
347 response.end(tlv.encode(6 /* TLVValues.STATE */, 2 /* PairingStates.M2 */, 7 /* TLVValues.ERROR_CODE */, 6 /* TLVErrorCode.UNAVAILABLE */));
348 return;
349 }
350 if (this.unsuccessfulPairAttempts > 100) {
351 debug("[%s] Reached maximum amount of unsuccessful pair attempts!", this.accessoryInfo.username);
352 response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" });
353 response.end(tlv.encode(6 /* TLVValues.STATE */, 2 /* PairingStates.M2 */, 7 /* TLVValues.ERROR_CODE */, 5 /* TLVErrorCode.MAX_TRIES */));
354 return;
355 }
356 const tlvData = tlv.decode(data);
357 const sequence = tlvData[6 /* TLVValues.SEQUENCE_NUM */][0]; // value is single byte with sequence number
358 if (sequence === 1 /* PairingStates.M1 */) {
359 this.handlePairSetupM1(connection, request, response);
360 }
361 else if (sequence === 3 /* PairingStates.M3 */ && connection._pairSetupState === 2 /* PairingStates.M2 */) {
362 this.handlePairSetupM3(connection, request, response, tlvData);
363 }
364 else if (sequence === 5 /* PairingStates.M5 */ && connection._pairSetupState === 4 /* PairingStates.M4 */) {
365 this.handlePairSetupM5(connection, request, response, tlvData);
366 }
367 else {
368 // Invalid state/sequence number
369 response.writeHead(400 /* HAPPairingHTTPCode.BAD_REQUEST */, { "Content-Type": "application/pairing+tlv8" });
370 response.end(tlv.encode(6 /* TLVValues.STATE */, sequence + 1, 7 /* TLVValues.ERROR_CODE */, 1 /* TLVErrorCode.UNKNOWN */));
371 return;
372 }
373 }
374 handlePairSetupM1(connection, request, response) {
375 debug("[%s] Pair step 1/5", this.accessoryInfo.username);
376 const salt = crypto_1.default.randomBytes(16);
377 const srpParams = fast_srp_hap_1.SRP.params.hap;
378 fast_srp_hap_1.SRP.genKey(32).then(key => {
379 // create a new SRP server
380 const srpServer = new fast_srp_hap_1.SrpServer(srpParams, salt, Buffer.from("Pair-Setup"), Buffer.from(this.accessoryInfo.pincode), key);
381 const srpB = srpServer.computeB();
382 // attach it to the current TCP session
383 connection.srpServer = srpServer;
384 response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" });
385 response.end(tlv.encode(6 /* TLVValues.SEQUENCE_NUM */, 2 /* PairingStates.M2 */, 2 /* TLVValues.SALT */, salt, 3 /* TLVValues.PUBLIC_KEY */, srpB));
386 connection._pairSetupState = 2 /* PairingStates.M2 */;
387 }).catch(error => {
388 debug("[%s] Error occurred when generating srp key: %s", this.accessoryInfo.username, error.message);
389 response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" });
390 response.end(tlv.encode(6 /* TLVValues.STATE */, 2 /* PairingStates.M2 */, 7 /* TLVValues.ERROR_CODE */, 1 /* TLVErrorCode.UNKNOWN */));
391 return;
392 });
393 }
394 handlePairSetupM3(connection, request, response, tlvData) {
395 debug("[%s] Pair step 2/5", this.accessoryInfo.username);
396 const A = tlvData[3 /* TLVValues.PUBLIC_KEY */]; // "A is a public key that exists only for a single login session."
397 const M1 = tlvData[4 /* TLVValues.PASSWORD_PROOF */]; // "M1 is the proof that you actually know your own password."
398 // pull the SRP server we created in stepOne out of the current session
399 const srpServer = connection.srpServer;
400 srpServer.setA(A);
401 try {
402 srpServer.checkM1(M1);
403 }
404 catch (err) {
405 // most likely the client supplied an incorrect pincode.
406 this.unsuccessfulPairAttempts++;
407 debug("[%s] Error while checking pincode: %s", this.accessoryInfo.username, err.message);
408 response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" });
409 response.end(tlv.encode(6 /* TLVValues.SEQUENCE_NUM */, 4 /* PairingStates.M4 */, 7 /* TLVValues.ERROR_CODE */, 2 /* TLVErrorCode.AUTHENTICATION */));
410 connection._pairSetupState = undefined;
411 return;
412 }
413 // "M2 is the proof that the server actually knows your password."
414 const M2 = srpServer.computeM2();
415 response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" });
416 response.end(tlv.encode(6 /* TLVValues.SEQUENCE_NUM */, 4 /* PairingStates.M4 */, 4 /* TLVValues.PASSWORD_PROOF */, M2));
417 connection._pairSetupState = 4 /* PairingStates.M4 */;
418 }
419 handlePairSetupM5(connection, request, response, tlvData) {
420 debug("[%s] Pair step 3/5", this.accessoryInfo.username);
421 // pull the SRP server we created in stepOne out of the current session
422 const srpServer = connection.srpServer;
423 const encryptedData = tlvData[5 /* TLVValues.ENCRYPTED_DATA */];
424 const messageData = Buffer.alloc(encryptedData.length - 16);
425 const authTagData = Buffer.alloc(16);
426 encryptedData.copy(messageData, 0, 0, encryptedData.length - 16);
427 encryptedData.copy(authTagData, 0, encryptedData.length - 16, encryptedData.length);
428 const S_private = srpServer.computeK();
429 const encSalt = Buffer.from("Pair-Setup-Encrypt-Salt");
430 const encInfo = Buffer.from("Pair-Setup-Encrypt-Info");
431 const outputKey = hapCrypto.HKDF("sha512", encSalt, S_private, encInfo, 32);
432 let plaintext;
433 try {
434 plaintext = hapCrypto.chacha20_poly1305_decryptAndVerify(outputKey, Buffer.from("PS-Msg05"), null, messageData, authTagData);
435 }
436 catch (error) {
437 debug("[%s] Error while decrypting and verifying M5 subTlv: %s", this.accessoryInfo.username);
438 response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" });
439 response.end(tlv.encode(6 /* TLVValues.SEQUENCE_NUM */, 4 /* PairingStates.M4 */, 7 /* TLVValues.ERROR_CODE */, 2 /* TLVErrorCode.AUTHENTICATION */));
440 connection._pairSetupState = undefined;
441 return;
442 }
443 // decode the client payload and pass it on to the next step
444 const M5Packet = tlv.decode(plaintext);
445 const clientUsername = M5Packet[1 /* TLVValues.USERNAME */];
446 const clientLTPK = M5Packet[3 /* TLVValues.PUBLIC_KEY */];
447 const clientProof = M5Packet[10 /* TLVValues.PROOF */];
448 this.handlePairSetupM5_2(connection, request, response, clientUsername, clientLTPK, clientProof, outputKey);
449 }
450 // M5-2
451 handlePairSetupM5_2(connection, request, response, clientUsername, clientLTPK, clientProof, hkdfEncKey) {
452 debug("[%s] Pair step 4/5", this.accessoryInfo.username);
453 const S_private = connection.srpServer.computeK();
454 const controllerSalt = Buffer.from("Pair-Setup-Controller-Sign-Salt");
455 const controllerInfo = Buffer.from("Pair-Setup-Controller-Sign-Info");
456 const outputKey = hapCrypto.HKDF("sha512", controllerSalt, S_private, controllerInfo, 32);
457 const completeData = Buffer.concat([outputKey, clientUsername, clientLTPK]);
458 if (!tweetnacl_1.default.sign.detached.verify(completeData, clientProof, clientLTPK)) {
459 debug("[%s] Invalid signature", this.accessoryInfo.username);
460 response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" });
461 response.end(tlv.encode(6 /* TLVValues.SEQUENCE_NUM */, 6 /* PairingStates.M6 */, 7 /* TLVValues.ERROR_CODE */, 2 /* TLVErrorCode.AUTHENTICATION */));
462 connection._pairSetupState = undefined;
463 return;
464 }
465 this.handlePairSetupM5_3(connection, request, response, clientUsername, clientLTPK, hkdfEncKey);
466 }
467 // M5 - F + M6
468 handlePairSetupM5_3(connection, request, response, clientUsername, clientLTPK, hkdfEncKey) {
469 debug("[%s] Pair step 5/5", this.accessoryInfo.username);
470 const S_private = connection.srpServer.computeK();
471 const accessorySalt = Buffer.from("Pair-Setup-Accessory-Sign-Salt");
472 const accessoryInfo = Buffer.from("Pair-Setup-Accessory-Sign-Info");
473 const outputKey = hapCrypto.HKDF("sha512", accessorySalt, S_private, accessoryInfo, 32);
474 const serverLTPK = this.accessoryInfo.signPk;
475 const usernameData = Buffer.from(this.accessoryInfo.username);
476 const material = Buffer.concat([outputKey, usernameData, serverLTPK]);
477 const privateKey = Buffer.from(this.accessoryInfo.signSk);
478 const serverProof = tweetnacl_1.default.sign.detached(material, privateKey);
479 const message = tlv.encode(1 /* TLVValues.USERNAME */, usernameData, 3 /* TLVValues.PUBLIC_KEY */, serverLTPK, 10 /* TLVValues.PROOF */, serverProof);
480 const encrypted = hapCrypto.chacha20_poly1305_encryptAndSeal(hkdfEncKey, Buffer.from("PS-Msg06"), null, message);
481 // finally, notify listeners that we have been paired with a client
482 this.emit("pair" /* HAPServerEventTypes.PAIR */, clientUsername.toString(), clientLTPK, (0, once_1.once)(err => {
483 if (err) {
484 debug("[%s] Error adding pairing info: %s", this.accessoryInfo.username, err.message);
485 response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" });
486 response.end(tlv.encode(6 /* TLVValues.SEQUENCE_NUM */, 6 /* PairingStates.M6 */, 7 /* TLVValues.ERROR_CODE */, 1 /* TLVErrorCode.UNKNOWN */));
487 connection._pairSetupState = undefined;
488 return;
489 }
490 // send final pairing response to client
491 response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" });
492 response.end(tlv.encode(6 /* TLVValues.SEQUENCE_NUM */, 6 /* PairingStates.M6 */, 5 /* TLVValues.ENCRYPTED_DATA */, Buffer.concat([encrypted.ciphertext, encrypted.authTag])));
493 connection._pairSetupState = undefined;
494 }));
495 }
496 handlePairVerify(connection, url, request, data, response) {
497 const tlvData = tlv.decode(data);
498 const sequence = tlvData[6 /* TLVValues.SEQUENCE_NUM */][0]; // value is single byte with sequence number
499 if (sequence === 1 /* PairingStates.M1 */) {
500 this.handlePairVerifyM1(connection, request, response, tlvData);
501 }
502 else if (sequence === 3 /* PairingStates.M3 */ && connection._pairVerifyState === 2 /* PairingStates.M2 */) {
503 this.handlePairVerifyM3(connection, request, response, tlvData);
504 }
505 else {
506 // Invalid state/sequence number
507 response.writeHead(400 /* HAPPairingHTTPCode.BAD_REQUEST */, { "Content-Type": "application/pairing+tlv8" });
508 response.end(tlv.encode(6 /* TLVValues.STATE */, sequence + 1, 7 /* TLVValues.ERROR_CODE */, 1 /* TLVErrorCode.UNKNOWN */));
509 return;
510 }
511 }
512 handlePairVerifyM1(connection, request, response, tlvData) {
513 debug("[%s] Pair verify step 1/2", this.accessoryInfo.username);
514 const clientPublicKey = tlvData[3 /* TLVValues.PUBLIC_KEY */]; // Buffer
515 // generate new encryption keys for this session
516 const keyPair = hapCrypto.generateCurve25519KeyPair();
517 const secretKey = Buffer.from(keyPair.secretKey);
518 const publicKey = Buffer.from(keyPair.publicKey);
519 const sharedSec = Buffer.from(hapCrypto.generateCurve25519SharedSecKey(secretKey, clientPublicKey));
520 const usernameData = Buffer.from(this.accessoryInfo.username);
521 const material = Buffer.concat([publicKey, usernameData, clientPublicKey]);
522 const privateKey = Buffer.from(this.accessoryInfo.signSk);
523 const serverProof = tweetnacl_1.default.sign.detached(material, privateKey);
524 const encSalt = Buffer.from("Pair-Verify-Encrypt-Salt");
525 const encInfo = Buffer.from("Pair-Verify-Encrypt-Info");
526 const outputKey = hapCrypto.HKDF("sha512", encSalt, sharedSec, encInfo, 32).slice(0, 32);
527 connection.encryption = new eventedhttp_1.HAPEncryption(clientPublicKey, secretKey, publicKey, sharedSec, outputKey);
528 // compose the response data in TLV format
529 const message = tlv.encode(1 /* TLVValues.USERNAME */, usernameData, 10 /* TLVValues.PROOF */, serverProof);
530 const encrypted = hapCrypto.chacha20_poly1305_encryptAndSeal(outputKey, Buffer.from("PV-Msg02"), null, message);
531 response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" });
532 response.end(tlv.encode(6 /* TLVValues.SEQUENCE_NUM */, 2 /* PairingStates.M2 */, 5 /* TLVValues.ENCRYPTED_DATA */, Buffer.concat([encrypted.ciphertext, encrypted.authTag]), 3 /* TLVValues.PUBLIC_KEY */, publicKey));
533 connection._pairVerifyState = 2 /* PairingStates.M2 */;
534 }
535 handlePairVerifyM3(connection, request, response, objects) {
536 debug("[%s] Pair verify step 2/2", this.accessoryInfo.username);
537 const encryptedData = objects[5 /* TLVValues.ENCRYPTED_DATA */];
538 const messageData = Buffer.alloc(encryptedData.length - 16);
539 const authTagData = Buffer.alloc(16);
540 encryptedData.copy(messageData, 0, 0, encryptedData.length - 16);
541 encryptedData.copy(authTagData, 0, encryptedData.length - 16, encryptedData.length);
542 // instance of HAPEncryption (created in handlePairVerifyStepOne)
543 const enc = connection.encryption;
544 let plaintext;
545 try {
546 plaintext = hapCrypto.chacha20_poly1305_decryptAndVerify(enc.hkdfPairEncryptionKey, Buffer.from("PV-Msg03"), null, messageData, authTagData);
547 }
548 catch (error) {
549 debug("[%s] M3: Failed to decrypt and/or verify", this.accessoryInfo.username);
550 response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" });
551 response.end(tlv.encode(6 /* TLVValues.STATE */, 4 /* PairingStates.M4 */, 7 /* TLVValues.ERROR_CODE */, 2 /* TLVErrorCode.AUTHENTICATION */));
552 connection._pairVerifyState = undefined;
553 return;
554 }
555 const decoded = tlv.decode(plaintext);
556 const clientUsername = decoded[1 /* TLVValues.USERNAME */];
557 const proof = decoded[10 /* TLVValues.PROOF */];
558 const material = Buffer.concat([enc.clientPublicKey, clientUsername, enc.publicKey]);
559 // since we're paired, we should have the public key stored for this client
560 const clientPublicKey = this.accessoryInfo.getClientPublicKey(clientUsername.toString());
561 // if we're not actually paired, then there's nothing to verify - this client thinks it's paired with us, but we
562 // disagree. Respond with invalid request (seems to match HomeKit Accessory Simulator behavior)
563 if (!clientPublicKey) {
564 debug("[%s] Client %s attempting to verify, but we are not paired; rejecting client", this.accessoryInfo.username, clientUsername);
565 response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" });
566 response.end(tlv.encode(6 /* TLVValues.STATE */, 4 /* PairingStates.M4 */, 7 /* TLVValues.ERROR_CODE */, 2 /* TLVErrorCode.AUTHENTICATION */));
567 connection._pairVerifyState = undefined;
568 return;
569 }
570 if (!tweetnacl_1.default.sign.detached.verify(material, proof, clientPublicKey)) {
571 debug("[%s] Client %s provided an invalid signature", this.accessoryInfo.username, clientUsername);
572 response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" });
573 response.end(tlv.encode(6 /* TLVValues.STATE */, 4 /* PairingStates.M4 */, 7 /* TLVValues.ERROR_CODE */, 2 /* TLVErrorCode.AUTHENTICATION */));
574 connection._pairVerifyState = undefined;
575 return;
576 }
577 debug("[%s] Client %s verification complete", this.accessoryInfo.username, clientUsername);
578 response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" });
579 response.end(tlv.encode(6 /* TLVValues.SEQUENCE_NUM */, 4 /* PairingStates.M4 */));
580 // now that the client has been verified, we must "upgrade" our pseudo-HTTP connection to include
581 // TCP-level encryption. We'll do this by adding some more encryption vars to the session, and using them
582 // in future calls to onEncrypt, onDecrypt.
583 const encSalt = Buffer.from("Control-Salt");
584 const infoRead = Buffer.from("Control-Read-Encryption-Key");
585 const infoWrite = Buffer.from("Control-Write-Encryption-Key");
586 enc.accessoryToControllerKey = hapCrypto.HKDF("sha512", encSalt, enc.sharedSecret, infoRead, 32);
587 enc.controllerToAccessoryKey = hapCrypto.HKDF("sha512", encSalt, enc.sharedSecret, infoWrite, 32);
588 // Our connection is now completely setup. We now want to subscribe this connection to special
589 connection.connectionAuthenticated(clientUsername.toString());
590 connection._pairVerifyState = undefined;
591 }
592 handlePairings(connection, url, request, data, response) {
593 // Only accept /pairing request if there is a secure session
594 if (!this.allowInsecureRequest && !connection.isAuthenticated()) {
595 response.writeHead(470 /* HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ });
596 response.end(JSON.stringify({ status: -70401 /* HAPStatus.INSUFFICIENT_PRIVILEGES */ }));
597 return;
598 }
599 const objects = tlv.decode(data);
600 const method = objects[0 /* TLVValues.METHOD */][0]; // value is single byte with request type
601 const state = objects[6 /* TLVValues.STATE */][0];
602 if (state !== 1 /* PairingStates.M1 */) {
603 return;
604 }
605 if (method === 3 /* PairMethods.ADD_PAIRING */) {
606 const identifier = objects[1 /* TLVValues.IDENTIFIER */].toString();
607 const publicKey = objects[3 /* TLVValues.PUBLIC_KEY */];
608 const permissions = objects[11 /* TLVValues.PERMISSIONS */][0];
609 this.emit("add-pairing" /* HAPServerEventTypes.ADD_PAIRING */, connection, identifier, publicKey, permissions, (0, once_1.once)((error) => {
610 if (error > 0) {
611 debug("[%s] Pairings: failed ADD_PAIRING with code %d", this.accessoryInfo.username, error);
612 response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" });
613 response.end(tlv.encode(6 /* TLVValues.STATE */, 2 /* PairingStates.M2 */, 7 /* TLVValues.ERROR_CODE */, error));
614 return;
615 }
616 response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" });
617 response.end(tlv.encode(6 /* TLVValues.STATE */, 2 /* PairingStates.M2 */));
618 debug("[%s] Pairings: successfully executed ADD_PAIRING", this.accessoryInfo.username);
619 }));
620 }
621 else if (method === 4 /* PairMethods.REMOVE_PAIRING */) {
622 const identifier = objects[1 /* TLVValues.IDENTIFIER */].toString();
623 this.emit("remove-pairing" /* HAPServerEventTypes.REMOVE_PAIRING */, connection, identifier, (0, once_1.once)((error) => {
624 if (error > 0) {
625 debug("[%s] Pairings: failed REMOVE_PAIRING with code %d", this.accessoryInfo.username, error);
626 response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" });
627 response.end(tlv.encode(6 /* TLVValues.STATE */, 2 /* PairingStates.M2 */, 7 /* TLVValues.ERROR_CODE */, error));
628 return;
629 }
630 response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" });
631 response.end(tlv.encode(6 /* TLVValues.STATE */, 2 /* PairingStates.M2 */));
632 debug("[%s] Pairings: successfully executed REMOVE_PAIRING", this.accessoryInfo.username);
633 }));
634 }
635 else if (method === 5 /* PairMethods.LIST_PAIRINGS */) {
636 this.emit("list-pairings" /* HAPServerEventTypes.LIST_PAIRINGS */, connection, (0, once_1.once)((error, data) => {
637 if (error > 0) {
638 debug("[%s] Pairings: failed LIST_PAIRINGS with code %d", this.accessoryInfo.username, error);
639 response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" });
640 response.end(tlv.encode(6 /* TLVValues.STATE */, 2 /* PairingStates.M2 */, 7 /* TLVValues.ERROR_CODE */, error));
641 return;
642 }
643 // eslint-disable-next-line @typescript-eslint/no-explicit-any
644 const tlvList = [];
645 data.forEach((value, index) => {
646 if (index > 0) {
647 tlvList.push(255 /* TLVValues.SEPARATOR */, Buffer.alloc(0));
648 }
649 tlvList.push(1 /* TLVValues.IDENTIFIER */, value.username, 3 /* TLVValues.PUBLIC_KEY */, value.publicKey, 11 /* TLVValues.PERMISSIONS */, value.permission);
650 });
651 const list = tlv.encode(6 /* TLVValues.STATE */, 2 /* PairingStates.M2 */, ...tlvList);
652 response.writeHead(200 /* HAPPairingHTTPCode.OK */, { "Content-Type": "application/pairing+tlv8" /* HAPMimeTypes.PAIRING_TLV8 */ });
653 response.end(list);
654 debug("[%s] Pairings: successfully executed LIST_PAIRINGS", this.accessoryInfo.username);
655 }));
656 }
657 }
658 handleAccessories(connection, url, request, data, response) {
659 if (!this.allowInsecureRequest && !connection.isAuthenticated()) {
660 response.writeHead(470 /* HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ });
661 response.end(JSON.stringify({ status: -70401 /* HAPStatus.INSUFFICIENT_PRIVILEGES */ }));
662 return;
663 }
664 // call out to listeners to retrieve the latest accessories JSON
665 this.emit("accessories" /* HAPServerEventTypes.ACCESSORIES */, connection, (0, once_1.once)((error, result) => {
666 if (error) {
667 response.writeHead(error.httpCode, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ });
668 response.end(JSON.stringify({ status: error.status }));
669 }
670 else {
671 response.writeHead(200 /* HAPHTTPCode.OK */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ });
672 response.end(JSON.stringify(result));
673 }
674 }));
675 }
676 handleCharacteristics(connection, url, request, data, response) {
677 if (!this.allowInsecureRequest && !connection.isAuthenticated()) {
678 response.writeHead(470 /* HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ });
679 response.end(JSON.stringify({ status: -70401 /* HAPStatus.INSUFFICIENT_PRIVILEGES */ }));
680 return;
681 }
682 if (request.method === "GET") {
683 const searchParams = url.searchParams;
684 const idParam = searchParams.get("id");
685 if (!idParam) {
686 response.writeHead(400 /* HAPHTTPCode.BAD_REQUEST */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ });
687 response.end(JSON.stringify({ status: -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */ }));
688 return;
689 }
690 const ids = [];
691 for (const entry of idParam.split(",")) { // ["1.9","2.14"]
692 const split = entry.split("."); // ["1","9"]
693 ids.push({
694 aid: parseInt(split[0], 10), // accessory id
695 iid: parseInt(split[1], 10), // (characteristic) instance id
696 });
697 }
698 const readRequest = {
699 ids: ids,
700 includeMeta: (0, internal_types_1.consideredTrue)(searchParams.get("meta")),
701 includePerms: (0, internal_types_1.consideredTrue)(searchParams.get("perms")),
702 includeType: (0, internal_types_1.consideredTrue)(searchParams.get("type")),
703 includeEvent: (0, internal_types_1.consideredTrue)(searchParams.get("ev")),
704 };
705 this.emit("get-characteristics" /* HAPServerEventTypes.GET_CHARACTERISTICS */, connection, readRequest, (0, once_1.once)((error, readResponse) => {
706 if (error) {
707 response.writeHead(error.httpCode, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ });
708 response.end(JSON.stringify({ status: error.status }));
709 return;
710 }
711 const characteristics = readResponse.characteristics;
712 let errorOccurred = false; // determine if we send a 207 Multi-Status
713 for (const data of characteristics) {
714 if (data.status) {
715 errorOccurred = true;
716 break;
717 }
718 }
719 if (errorOccurred) { // on a 207 Multi-Status EVERY characteristic MUST include a status property
720 for (const data of characteristics) {
721 if (!data.status) { // a status is undefined if the request was successful
722 data.status = 0 /* HAPStatus.SUCCESS */; // a value of zero indicates success
723 }
724 }
725 }
726 // 207 "multi-status" is returned when an error occurs reading a characteristic. otherwise 200 is returned
727 response.writeHead(errorOccurred ? 207 /* HAPHTTPCode.MULTI_STATUS */ : 200 /* HAPHTTPCode.OK */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ });
728 response.end(JSON.stringify({ characteristics: characteristics }));
729 }));
730 }
731 else if (request.method === "PUT") {
732 if (!connection.isAuthenticated()) {
733 if (!request.headers || (request.headers && request.headers.authorization !== this.accessoryInfo.pincode)) {
734 response.writeHead(470 /* HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ });
735 response.end(JSON.stringify({ status: -70401 /* HAPStatus.INSUFFICIENT_PRIVILEGES */ }));
736 return;
737 }
738 }
739 if (data.length === 0) {
740 response.writeHead(400 /* HAPHTTPCode.BAD_REQUEST */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ });
741 response.end(JSON.stringify({ status: -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */ }));
742 return;
743 }
744 const writeRequest = JSON.parse(data.toString("utf8"));
745 this.emit("set-characteristics" /* HAPServerEventTypes.SET_CHARACTERISTICS */, connection, writeRequest, (0, once_1.once)((error, writeResponse) => {
746 if (error) {
747 response.writeHead(error.httpCode, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ });
748 response.end(JSON.stringify({ status: error.status }));
749 return;
750 }
751 const characteristics = writeResponse.characteristics;
752 let multiStatus = false;
753 for (const data of characteristics) {
754 if (data.status || data.value !== undefined) {
755 // also send multiStatus on write response requests
756 multiStatus = true;
757 break;
758 }
759 }
760 if (multiStatus) {
761 // 207 is "multi-status" since HomeKit may be setting multiple things and any one can fail independently
762 response.writeHead(207 /* HAPHTTPCode.MULTI_STATUS */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ });
763 response.end(JSON.stringify({ characteristics: characteristics }));
764 }
765 else {
766 // if everything went fine send 204 no content response
767 response.writeHead(204 /* HAPHTTPCode.NO_CONTENT */);
768 response.end();
769 }
770 }));
771 }
772 else {
773 response.writeHead(400 /* HAPHTTPCode.BAD_REQUEST */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); // method not allowed
774 response.end(JSON.stringify({ status: -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */ }));
775 }
776 }
777 handlePrepareWrite(connection, url, request, data, response) {
778 if (!this.allowInsecureRequest && !connection.isAuthenticated()) {
779 response.writeHead(470 /* HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ });
780 response.end(JSON.stringify({ status: -70401 /* HAPStatus.INSUFFICIENT_PRIVILEGES */ }));
781 return;
782 }
783 if (request.method === "PUT") {
784 if (data.length === 0) {
785 response.writeHead(400 /* HAPHTTPCode.BAD_REQUEST */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ });
786 response.end(JSON.stringify({ status: -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */ }));
787 return;
788 }
789 const prepareRequest = JSON.parse(data.toString());
790 if (prepareRequest.pid && prepareRequest.ttl) {
791 debug("[%s] Received prepare write request with pid %d and ttl %d", this.accessoryInfo.username, prepareRequest.pid, prepareRequest.ttl);
792 if (connection.timedWriteTimeout) { // clear any currently existing timeouts
793 clearTimeout(connection.timedWriteTimeout);
794 }
795 connection.timedWritePid = prepareRequest.pid;
796 connection.timedWriteTimeout = setTimeout(() => {
797 debug("[%s] Timed write request timed out for pid %d", this.accessoryInfo.username, prepareRequest.pid);
798 connection.timedWritePid = undefined;
799 connection.timedWriteTimeout = undefined;
800 }, prepareRequest.ttl);
801 response.writeHead(200 /* HAPHTTPCode.OK */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ });
802 response.end(JSON.stringify({ status: 0 /* HAPStatus.SUCCESS */ }));
803 return;
804 }
805 else {
806 response.writeHead(400 /* HAPHTTPCode.BAD_REQUEST */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ });
807 response.end(JSON.stringify({ status: -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */ }));
808 }
809 }
810 else {
811 response.writeHead(400 /* HAPHTTPCode.BAD_REQUEST */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ });
812 response.end(JSON.stringify({ status: -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */ }));
813 }
814 }
815 handleResource(connection, url, request, data, response) {
816 if (!connection.isAuthenticated()) {
817 if (!(this.allowInsecureRequest && request.headers && request.headers.authorization === this.accessoryInfo.pincode)) {
818 response.writeHead(470 /* HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ });
819 response.end(JSON.stringify({ status: -70401 /* HAPStatus.INSUFFICIENT_PRIVILEGES */ }));
820 return;
821 }
822 }
823 if (request.method === "POST") {
824 if (data.length === 0) {
825 response.writeHead(400 /* HAPHTTPCode.BAD_REQUEST */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ });
826 response.end(JSON.stringify({ status: -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */ }));
827 return;
828 }
829 const resourceRequest = JSON.parse(data.toString());
830 // call out to listeners to retrieve the resource, snapshot only right now
831 this.emit("request-resource" /* HAPServerEventTypes.REQUEST_RESOURCE */, resourceRequest, (0, once_1.once)((error, resource) => {
832 if (error) {
833 response.writeHead(error.httpCode, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ });
834 response.end(JSON.stringify({ status: error.status }));
835 }
836 else {
837 response.writeHead(200 /* HAPHTTPCode.OK */, { "Content-Type": "image/jpeg" /* HAPMimeTypes.IMAGE_JPEG */ });
838 response.end(resource);
839 }
840 }));
841 }
842 else {
843 response.writeHead(400 /* HAPHTTPCode.BAD_REQUEST */, { "Content-Type": "application/hap+json" /* HAPMimeTypes.HAP_JSON */ }); // method not allowed
844 response.end(JSON.stringify({ status: -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */ }));
845 }
846 }
847}
848exports.HAPServer = HAPServer;
849//# sourceMappingURL=HAPServer.js.map
\No newline at end of file