├── .gitignore ├── BridgedCore.js ├── Core.js ├── LICENSE ├── README.md ├── accessories ├── BedroomLight_accessory.js ├── ChristmasLight_accessory.js ├── Fountain_accessory.js ├── NurseryLight_accessory.js ├── NurseryTemperatureSensor_accessory.js └── types.js ├── index.js ├── lib ├── Accessory.js ├── AccessoryLoader.js ├── Advertiser.js ├── Bridge.js ├── Characteristic.js ├── HAPServer.js ├── Service.js ├── gen │ ├── HomeKitTypes.js │ └── import.js ├── model │ ├── AccessoryInfo.js │ └── IdentifierCache.js └── util │ ├── chacha20poly1305.js │ ├── clone.js │ ├── encryption.js │ ├── eventedhttp.js │ ├── hkdf.js │ ├── once.js │ ├── tlv.js │ └── uuid.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | persist 2 | node_modules 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /BridgedCore.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var storage = require('node-persist'); 4 | var uuid = require('./').uuid; 5 | var Bridge = require('./').Bridge; 6 | var Accessory = require('./').Accessory; 7 | var accessoryLoader = require('./lib/AccessoryLoader'); 8 | 9 | console.log("HAP-NodeJS starting..."); 10 | 11 | // Initialize our storage system 12 | storage.initSync(); 13 | 14 | // Start by creating our Bridge which will host all loaded Accessories 15 | var bridge = new Bridge('Node Bridge', uuid.generate("Node Bridge")); 16 | 17 | // Listen for bridge identification event 18 | bridge.on('identify', function(paired, callback) { 19 | console.log("Node Bridge identify"); 20 | callback(); // success 21 | }); 22 | 23 | // Load up all accessories in the /accessories folder 24 | var dir = path.join(__dirname, "accessories"); 25 | var accessories = accessoryLoader.loadDirectory(dir); 26 | 27 | // Add them all to the bridge 28 | accessories.forEach(function(accessory) { 29 | bridge.addBridgedAccessory(accessory); 30 | }); 31 | 32 | // Publish the Bridge on the local network. 33 | bridge.publish({ 34 | username: "CC:22:3D:E3:CE:F6", 35 | port: 51826, 36 | pincode: "031-45-154", 37 | category: Accessory.Categories.BRIDGE 38 | }); 39 | 40 | 41 | -------------------------------------------------------------------------------- /Core.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var storage = require('node-persist'); 3 | var uuid = require('./').uuid; 4 | var Accessory = require('./').Accessory; 5 | var accessoryLoader = require('./lib/AccessoryLoader'); 6 | 7 | console.log("HAP-NodeJS starting..."); 8 | 9 | // Initialize our storage system 10 | storage.initSync(); 11 | 12 | // Our Accessories will each have their own HAP server; we will assign ports sequentially 13 | var targetPort = 51826; 14 | 15 | // Load up all accessories in the /accessories folder 16 | var dir = path.join(__dirname, "accessories"); 17 | var accessories = accessoryLoader.loadDirectory(dir); 18 | 19 | // Publish them all separately (as opposed to BridgedCore which publishes them behind a single Bridge accessory) 20 | accessories.forEach(function(accessory) { 21 | 22 | // To push Accessories separately, we'll need a few extra properties 23 | if (!accessory.username) 24 | throw new Error("Username not found on accessory '" + accessory.displayName + 25 | "'. Core.js requires all accessories to define a unique 'username' property."); 26 | 27 | if (!accessory.pincode) 28 | throw new Error("Pincode not found on accessory '" + accessory.displayName + 29 | "'. Core.js requires all accessories to define a 'pincode' property."); 30 | 31 | // publish this Accessory on the local network 32 | accessory.publish({ 33 | port: targetPort++, 34 | username: accessory.username, 35 | pincode: accessory.pincode 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | HAP-NodeJS 2 | ========== 3 | 4 | HAP-NodeJS is a Node.js implementation of HomeKit Accessory Server. 5 | 6 | With this project, you should be able to create your own HomeKit Accessory on Raspberry Pi, Intel Edison or any other platform that can run Node.js :) 7 | 8 | The implementation may not 100% follow the HAP MFi Specification since MFi program doesn't allow individual developer to join. 9 | 10 | Remember to run `npm install` before actually running the server. 11 | 12 | Users can define their own accessories in: accessories/*name*_accessory.js files, where name is a short description of the accessory. All defined accessories get loaded on server start. You can define accessories using an object literal notation (see [Fan_accessory.js](accessories/Fan_accessory.js) for an example) or you can use the API (see below). 13 | 14 | You can use the following command to start the HAP Server in Bridged mode: 15 | 16 | ```sh 17 | node BridgedCore.js 18 | ``` 19 | 20 | Or if you wish to host each Accessory as an independent HomeKit device: 21 | 22 | ```sh 23 | node Core.js 24 | ``` 25 | 26 | The HAP-NodeJS library uses the [debug](https://github.com/visionmedia/debug) library for log output. You can print some or all logs by setting the `DEBUG` environment variable. For instance, to see all debug logs while running the server: 27 | 28 | ```sh 29 | DEBUG=* node BridgedCore.js 30 | ``` 31 | 32 | API 33 | === 34 | 35 | HAP-NodeJS provides a set of classes you can use to construct Accessories programatically. For an example implementation, see [Lock_accessory.js](accessories/Lock_accessory.js). 36 | 37 | The key classes intended for use by API consumers are: 38 | 39 | * [Accessory](lib/Accessory.js): Represents a HomeKit device that can be published on your local network. 40 | * [Bridge](lib/Bridge.js): A kind of Accessory that can host other Accessories "behind" it while only publishing a single device. 41 | * [Service](lib/Service.js): Represents a set of grouped values necessary to provide a logical function. Most of the time, when you think of a supported HomeKit device like "Thermostat" or "Door Lock", you're actualy thinking of a Service. Accessories can expose multiple services. 42 | * [Characteristic](lib/Characteristic.js): Represents a particular typed variable assigned to a Service, for instance the `LockMechanism` Service contains a `CurrentDoorState` Characteristic describing whether the door is currently locked. 43 | 44 | All known built-in Service and Characteristic types that HomeKit supports are exposed as a separate subclass in [HomeKitTypes](lib/gen/HomeKitTypes.js). 45 | 46 | See each of the corresponding class files for more explanation and notes. 47 | 48 | Notes 49 | ===== 50 | 51 | Special thanks to [Alex Skalozub](https://twitter.com/pieceofsummer), who reverse engineered the server side HAP. ~~You can find his research at [here](https://gist.github.com/pieceofsummer/13272bf76ac1d6b58a30).~~ (Sadly, on Nov 4, Apple sent the [DMCA](https://github.com/github/dmca/blob/master/2014-11-04-Apple.md) request to Github to remove the research.) 52 | 53 | [There](http://instagram.com/p/t4cPlcDksQ/) is a video demo running this project on Intel Edison. 54 | 55 | If you are interested in HAP over BTLE, you might want to check [this](https://gist.github.com/KhaosT/6ff09ba71d306d4c1079). 56 | -------------------------------------------------------------------------------- /accessories/BedroomLight_accessory.js: -------------------------------------------------------------------------------- 1 | // MQTT Setup 2 | var mqtt = require('mqtt'); 3 | console.log("Connecting to MQTT broker..."); 4 | var mqtt = require('mqtt'); 5 | var options = { 6 | port: 1883, 7 | host: '192.168.1.12', 8 | clientId: 'AdyPi_BedroomLight' 9 | }; 10 | var client = mqtt.connect(options); 11 | console.log("BedroomLight Connected to MQTT broker"); 12 | 13 | 14 | var Accessory = require('../').Accessory; 15 | var Service = require('../').Service; 16 | var Characteristic = require('../').Characteristic; 17 | var uuid = require('../').uuid; 18 | 19 | // here's a fake hardware device that we'll expose to HomeKit 20 | var BEDROOM_LIGHT = { 21 | powerOn: false, 22 | brightness: 100, // percentage 23 | hue: 0, 24 | saturation: 0, 25 | 26 | setPowerOn: function(on) { 27 | console.log("Turning BedroomLight %s!", on ? "on" : "off"); 28 | 29 | if (on) { 30 | client.publish('BedroomLight', 'on'); 31 | BEDROOM_LIGHT.powerOn = on; 32 | } 33 | else { 34 | client.publish('BedroomLight','off'); 35 | BEDROOM_LIGHT.powerOn = false; 36 | }; 37 | 38 | }, 39 | setBrightness: function(brightness) { 40 | console.log("Setting light brightness to %s", brightness); 41 | client.publish('BedroomLightBrightness',String(brightness)); 42 | BEDROOM_LIGHT.brightness = brightness; 43 | }, 44 | setHue: function(hue){ 45 | console.log("Setting light Hue to %s", hue); 46 | client.publish('BedroomLightHue',String(hue)); 47 | BEDROOM_LIGHT.hue = hue; 48 | }, 49 | setSaturation: function(saturation){ 50 | console.log("Setting light Saturation to %s", saturation); 51 | client.publish('BedroomLightSaturation',String(saturation)); 52 | BEDROOM_LIGHT.saturation = saturation; 53 | }, 54 | identify: function() { 55 | console.log("Identify the light!"); 56 | } 57 | } 58 | 59 | // Generate a consistent UUID for our light Accessory that will remain the same even when 60 | // restarting our server. We use the `uuid.generate` helper function to create a deterministic 61 | // UUID based on an arbitrary "namespace" and the word "BedroomLight". 62 | var lightUUID = uuid.generate('hap-nodejs:accessories:BedroomLight'); 63 | 64 | // This is the Accessory that we'll return to HAP-NodeJS that represents our fake light. 65 | var light = exports.accessory = new Accessory('Light', lightUUID); 66 | 67 | // Add properties for publishing (in case we're using Core.js and not BridgedCore.js) 68 | light.username = "1A:2B:3C:4D:5E:FF"; 69 | light.pincode = "031-45-154"; 70 | 71 | // set some basic properties (these values are arbitrary and setting them is optional) 72 | light 73 | .getService(Service.AccessoryInformation) 74 | .setCharacteristic(Characteristic.Manufacturer, "Oltica") 75 | .setCharacteristic(Characteristic.Model, "Rev-1") 76 | .setCharacteristic(Characteristic.SerialNumber, "A1S2NASF88EW"); 77 | 78 | // listen for the "identify" event for this Accessory 79 | light.on('identify', function(paired, callback) { 80 | BEDROOM_LIGHT.identify(); 81 | callback(); // success 82 | }); 83 | 84 | // Add the actual Lightbulb Service and listen for change events from iOS. 85 | // We can see the complete list of Services and Characteristics in `lib/gen/HomeKitTypes.js` 86 | light 87 | .addService(Service.Lightbulb, "Bedroom Light") // services exposed to the user should have "names" like "Fake Light" for us 88 | .getCharacteristic(Characteristic.On) 89 | .on('set', function(value, callback) { 90 | BEDROOM_LIGHT.setPowerOn(value); 91 | callback(); // Our fake Light is synchronous - this value has been successfully set 92 | }); 93 | 94 | // We want to intercept requests for our current power state so we can query the hardware itself instead of 95 | // allowing HAP-NodeJS to return the cached Characteristic.value. 96 | light 97 | .getService(Service.Lightbulb) 98 | .getCharacteristic(Characteristic.On) 99 | .on('get', function(callback) { 100 | 101 | // this event is emitted when you ask Siri directly whether your light is on or not. you might query 102 | // the light hardware itself to find this out, then call the callback. But if you take longer than a 103 | // few seconds to respond, Siri will give up. 104 | 105 | var err = null; // in case there were any problems 106 | 107 | if (BEDROOM_LIGHT.powerOn) { 108 | console.log("Are we on? Yes."); 109 | callback(err, true); 110 | } 111 | else { 112 | console.log("Are we on? No."); 113 | callback(err, false); 114 | } 115 | }); 116 | 117 | // also add an "optional" Characteristic for Brightness 118 | light 119 | .getService(Service.Lightbulb) 120 | .addCharacteristic(Characteristic.Brightness) 121 | .on('get', function(callback) { 122 | callback(null, BEDROOM_LIGHT.brightness); 123 | }) 124 | .on('set', function(value, callback) { 125 | BEDROOM_LIGHT.setBrightness(value); 126 | callback(); 127 | }) 128 | 129 | light 130 | .getService(Service.Lightbulb) 131 | .addCharacteristic(Characteristic.Hue) 132 | .on('get',function(callback){ 133 | callback(null,BEDROOM_LIGHT.hue); 134 | }) 135 | .on('set',function(value,callback){ 136 | BEDROOM_LIGHT.setHue(value); 137 | callback(); 138 | }) 139 | 140 | light 141 | .getService(Service.Lightbulb) 142 | .addCharacteristic(Characteristic.Saturation) 143 | .on('get',function(callback){ 144 | callback(null,BEDROOM_LIGHT.saturation); 145 | }) 146 | .on('set',function(value,callback){ 147 | BEDROOM_LIGHT.setSaturation(value); 148 | callback(); 149 | }) 150 | -------------------------------------------------------------------------------- /accessories/ChristmasLight_accessory.js: -------------------------------------------------------------------------------- 1 | // MQTT Setup 2 | var mqtt = require('mqtt'); 3 | console.log("Connecting to MQTT broker..."); 4 | var mqtt = require('mqtt'); 5 | var options = { 6 | port: 1883, 7 | host: '192.168.1.12', 8 | clientId: 'AdyPi_ChristmasLight' 9 | }; 10 | var client = mqtt.connect(options); 11 | console.log("ChristmasLight Connected to MQTT broker"); 12 | 13 | var Accessory = require('../').Accessory; 14 | var Service = require('../').Service; 15 | var Characteristic = require('../').Characteristic; 16 | var uuid = require('../').uuid; 17 | 18 | // here's a fake hardware device that we'll expose to HomeKit 19 | var CHRISTMAS_LIGHT = { 20 | powerOn: false, 21 | 22 | setPowerOn: function(on) { 23 | console.log("Turning the Christmas light %s!", on ? "on" : "off"); 24 | if (on) { 25 | client.publish('ChristmasLight', 'on'); 26 | CHRISTMAS_LIGHT.powerOn = on; 27 | } 28 | else { 29 | client.publish('ChristmasLight','off'); 30 | CHRISTMAS_LIGHT.powerOn = false; 31 | }; 32 | 33 | }, 34 | identify: function() { 35 | console.log("Identify the light!"); 36 | } 37 | } 38 | 39 | // Generate a consistent UUID for our light Accessory that will remain the same even when 40 | // restarting our server. We use the `uuid.generate` helper function to create a deterministic 41 | // UUID based on an arbitrary "namespace" and the word "Christmaslight". 42 | var lightUUID = uuid.generate('hap-nodejs:accessories:Christmaslight'); 43 | 44 | // This is the Accessory that we'll return to HAP-NodeJS that represents our fake light. 45 | var light = exports.accessory = new Accessory('Light', lightUUID); 46 | 47 | // Add properties for publishing (in case we're using Core.js and not BridgedCore.js) 48 | light.username = "2A:2B:3C:4D:5E:FF"; 49 | light.pincode = "031-45-154"; 50 | 51 | // set some basic properties (these values are arbitrary and setting them is optional) 52 | light 53 | .getService(Service.AccessoryInformation) 54 | .setCharacteristic(Characteristic.Manufacturer, "Oltica") 55 | .setCharacteristic(Characteristic.Model, "Rev-1") 56 | .setCharacteristic(Characteristic.SerialNumber, "A1S2NASF88EW"); 57 | 58 | // listen for the "identify" event for this Accessory 59 | light.on('identify', function(paired, callback) { 60 | CHRISTMAS_LIGHT.identify(); 61 | callback(); // success 62 | }); 63 | 64 | // Add the actual Lightbulb Service and listen for change events from iOS. 65 | // We can see the complete list of Services and Characteristics in `lib/gen/HomeKitTypes.js` 66 | light 67 | .addService(Service.Lightbulb, "Christmas Light") // services exposed to the user should have "names" like "Fake Light" for us 68 | .getCharacteristic(Characteristic.On) 69 | .on('set', function(value, callback) { 70 | CHRISTMAS_LIGHT.setPowerOn(value); 71 | callback(); // Our fake Light is synchronous - this value has been successfully set 72 | }); 73 | 74 | // We want to intercept requests for our current power state so we can query the hardware itself instead of 75 | // allowing HAP-NodeJS to return the cached Characteristic.value. 76 | light 77 | .getService(Service.Lightbulb) 78 | .getCharacteristic(Characteristic.On) 79 | .on('get', function(callback) { 80 | 81 | // this event is emitted when you ask Siri directly whether your light is on or not. you might query 82 | // the light hardware itself to find this out, then call the callback. But if you take longer than a 83 | // few seconds to respond, Siri will give up. 84 | 85 | var err = null; // in case there were any problems 86 | 87 | if (CHRISTMAS_LIGHT.powerOn) { 88 | console.log("Are we on? Yes."); 89 | callback(err, true); 90 | } 91 | else { 92 | console.log("Are we on? No."); 93 | callback(err, false); 94 | } 95 | }); 96 | 97 | -------------------------------------------------------------------------------- /accessories/Fountain_accessory.js: -------------------------------------------------------------------------------- 1 | // MQTT Setup 2 | var mqtt = require('mqtt'); 3 | console.log("Connecting to MQTT broker..."); 4 | var mqtt = require('mqtt'); 5 | var options = { 6 | port: 1883, 7 | host: '192.168.1.12', 8 | clientId: 'AdyPi_Fountain' 9 | }; 10 | var client = mqtt.connect(options); 11 | console.log("Fountain Connected to MQTT broker"); 12 | 13 | 14 | var Accessory = require('../').Accessory; 15 | var Service = require('../').Service; 16 | var Characteristic = require('../').Characteristic; 17 | var uuid = require('../').uuid; 18 | 19 | // here's a fake hardware device that we'll expose to HomeKit 20 | var FOUNTAIN_LIGHT = { 21 | powerOn: false, 22 | brightness: 100, // percentage 23 | hue: 0, 24 | saturation: 0, 25 | 26 | setPowerOn: function(on) { 27 | console.log("Turning Fountain %s!", on ? "on" : "off"); 28 | 29 | if (on) { 30 | client.publish('Fountain', 'on'); 31 | FOUNTAIN_LIGHT.powerOn = on; 32 | } 33 | else { 34 | client.publish('Fountain','off'); 35 | FOUNTAIN_LIGHT.powerOn = false; 36 | }; 37 | 38 | }, 39 | setBrightness: function(brightness) { 40 | console.log("Setting light brightness to %s", brightness); 41 | client.publish('FountainBrightness',String(brightness)); 42 | FOUNTAIN_LIGHT.brightness = brightness; 43 | }, 44 | setHue: function(hue){ 45 | console.log("Setting light Hue to %s", hue); 46 | client.publish('FountainHue',String(hue)); 47 | FOUNTAIN_LIGHT.hue = hue; 48 | }, 49 | setSaturation: function(saturation){ 50 | console.log("Setting light Saturation to %s", saturation); 51 | client.publish('FountainSaturation',String(saturation)); 52 | FOUNTAIN_LIGHT.saturation = saturation; 53 | }, 54 | identify: function() { 55 | console.log("Identify the light!"); 56 | } 57 | } 58 | 59 | // Generate a consistent UUID for our light Accessory that will remain the same even when 60 | // restarting our server. We use the `uuid.generate` helper function to create a deterministic 61 | // UUID based on an arbitrary "namespace" and the word "Fountain". 62 | var lightUUID = uuid.generate('hap-nodejs:accessories:Fountain'); 63 | 64 | // This is the Accessory that we'll return to HAP-NodeJS that represents our fake light. 65 | var light = exports.accessory = new Accessory('Light', lightUUID); 66 | 67 | // Add properties for publishing (in case we're using Core.js and not BridgedCore.js) 68 | light.username = "1A:2B:3C:5D:6E:FF"; 69 | light.pincode = "031-45-154"; 70 | 71 | // set some basic properties (these values are arbitrary and setting them is optional) 72 | light 73 | .getService(Service.AccessoryInformation) 74 | .setCharacteristic(Characteristic.Manufacturer, "Oltica") 75 | .setCharacteristic(Characteristic.Model, "Rev-1") 76 | .setCharacteristic(Characteristic.SerialNumber, "A1S2NASF88EW"); 77 | 78 | // listen for the "identify" event for this Accessory 79 | light.on('identify', function(paired, callback) { 80 | FOUNTAIN_LIGHT.identify(); 81 | callback(); // success 82 | }); 83 | 84 | // Add the actual Lightbulb Service and listen for change events from iOS. 85 | // We can see the complete list of Services and Characteristics in `lib/gen/HomeKitTypes.js` 86 | light 87 | .addService(Service.Lightbulb, "Piano Light") // services exposed to the user should have "names" like "Fake Light" for us 88 | .getCharacteristic(Characteristic.On) 89 | .on('set', function(value, callback) { 90 | FOUNTAIN_LIGHT.setPowerOn(value); 91 | callback(); // Our fake Light is synchronous - this value has been successfully set 92 | }); 93 | 94 | // We want to intercept requests for our current power state so we can query the hardware itself instead of 95 | // allowing HAP-NodeJS to return the cached Characteristic.value. 96 | light 97 | .getService(Service.Lightbulb) 98 | .getCharacteristic(Characteristic.On) 99 | .on('get', function(callback) { 100 | 101 | // this event is emitted when you ask Siri directly whether your light is on or not. you might query 102 | // the light hardware itself to find this out, then call the callback. But if you take longer than a 103 | // few seconds to respond, Siri will give up. 104 | 105 | var err = null; // in case there were any problems 106 | 107 | if (FOUNTAIN_LIGHT.powerOn) { 108 | console.log("Are we on? Yes."); 109 | callback(err, true); 110 | } 111 | else { 112 | console.log("Are we on? No."); 113 | callback(err, false); 114 | } 115 | }); 116 | 117 | // also add an "optional" Characteristic for Brightness 118 | light 119 | .getService(Service.Lightbulb) 120 | .addCharacteristic(Characteristic.Brightness) 121 | .on('get', function(callback) { 122 | callback(null, FOUNTAIN_LIGHT.brightness); 123 | }) 124 | .on('set', function(value, callback) { 125 | FOUNTAIN_LIGHT.setBrightness(value); 126 | callback(); 127 | }) 128 | 129 | light 130 | .getService(Service.Lightbulb) 131 | .addCharacteristic(Characteristic.Hue) 132 | .on('get',function(callback){ 133 | callback(null,FOUNTAIN_LIGHT.hue); 134 | }) 135 | .on('set',function(value,callback){ 136 | FOUNTAIN_LIGHT.setHue(value); 137 | callback(); 138 | }) 139 | 140 | light 141 | .getService(Service.Lightbulb) 142 | .addCharacteristic(Characteristic.Saturation) 143 | .on('get',function(callback){ 144 | callback(null,FOUNTAIN_LIGHT.saturation); 145 | }) 146 | .on('set',function(value,callback){ 147 | FOUNTAIN_LIGHT.setSaturation(value); 148 | callback(); 149 | }) 150 | -------------------------------------------------------------------------------- /accessories/NurseryLight_accessory.js: -------------------------------------------------------------------------------- 1 | // MQTT Setup 2 | var mqtt = require('mqtt'); 3 | console.log("Connecting to MQTT broker..."); 4 | var mqtt = require('mqtt'); 5 | var options = { 6 | port: 1883, 7 | host: '192.168.1.12', 8 | clientId: 'AdyPi_NurseryLight' 9 | }; 10 | var client = mqtt.connect(options); 11 | console.log("Connected"); 12 | 13 | 14 | var Accessory = require('../').Accessory; 15 | var Service = require('../').Service; 16 | var Characteristic = require('../').Characteristic; 17 | var uuid = require('../').uuid; 18 | 19 | // here's a fake hardware device that we'll expose to HomeKit 20 | var NURSERY_LIGHT = { 21 | powerOn: false, 22 | brightness: 100, // percentage 23 | hue: 0, 24 | saturation: 0, 25 | 26 | setPowerOn: function(on) { 27 | console.log("Turning Nursery light %s!", on ? "on" : "off"); 28 | if (on) { 29 | client.publish('NurseryLight', 'on'); 30 | NURSERY_LIGHT.powerOn = on; 31 | } 32 | else { 33 | client.publish('NurseryLight','off'); 34 | NURSERY_LIGHT.powerOn = false; 35 | }; 36 | }, 37 | setBrightness: function(brightness) { 38 | console.log("Setting light brightness to %s", brightness); 39 | client.publish('BedroomLightBrightness',String(brightness)); 40 | NURSERY_LIGHT.brightness = brightness; 41 | }, 42 | setHue: function(hue){ 43 | console.log("Setting light Hue to %s", hue); 44 | client.publish('NurseryLightHue',String(hue)); 45 | NURSERY_LIGHT.hue = hue; 46 | }, 47 | setSaturation: function(saturation){ 48 | console.log("Setting light Saturation to %s", saturation); 49 | client.publish('NurseryLightSaturation',String(saturation)); 50 | NURSERY_LIGHT.saturation = saturation; 51 | }, 52 | identify: function() { 53 | console.log("Identify the light!"); 54 | } 55 | } 56 | 57 | // Generate a consistent UUID for our light Accessory that will remain the same even when 58 | // restarting our server. We use the `uuid.generate` helper function to create a deterministic 59 | // UUID based on an arbitrary "namespace" and the word "Nurserylight". 60 | var lightUUID = uuid.generate('hap-nodejs:accessories:Nurserylight'); 61 | 62 | // This is the Accessory that we'll return to HAP-NodeJS that represents our fake light. 63 | var light = exports.accessory = new Accessory('Light', lightUUID); 64 | 65 | // Add properties for publishing (in case we're using Core.js and not BridgedCore.js) 66 | light.username = "3A:2B:3C:4D:5E:FF"; 67 | light.pincode = "031-45-154"; 68 | 69 | // set some basic properties (these values are arbitrary and setting them is optional) 70 | light 71 | .getService(Service.AccessoryInformation) 72 | .setCharacteristic(Characteristic.Manufacturer, "Oltica") 73 | .setCharacteristic(Characteristic.Model, "Rev-1") 74 | .setCharacteristic(Characteristic.SerialNumber, "A1S2NASF88EW"); 75 | 76 | // listen for the "identify" event for this Accessory 77 | light.on('identify', function(paired, callback) { 78 | NURSERY_LIGHT.identify(); 79 | callback(); // success 80 | }); 81 | 82 | // Add the actual Lightbulb Service and listen for change events from iOS. 83 | // We can see the complete list of Services and Characteristics in `lib/gen/HomeKitTypes.js` 84 | light 85 | .addService(Service.Lightbulb, "Nursery Light") // services exposed to the user should have "names" like "Fake Light" for us 86 | .getCharacteristic(Characteristic.On) 87 | .on('set', function(value, callback) { 88 | NURSERY_LIGHT.setPowerOn(value); 89 | callback(); // Our fake Light is synchronous - this value has been successfully set 90 | }); 91 | 92 | // We want to intercept requests for our current power state so we can query the hardware itself instead of 93 | // allowing HAP-NodeJS to return the cached Characteristic.value. 94 | light 95 | .getService(Service.Lightbulb) 96 | .getCharacteristic(Characteristic.On) 97 | .on('get', function(callback) { 98 | 99 | // this event is emitted when you ask Siri directly whether your light is on or not. you might query 100 | // the light hardware itself to find this out, then call the callback. But if you take longer than a 101 | // few seconds to respond, Siri will give up. 102 | 103 | var err = null; // in case there were any problems 104 | 105 | if (NURSERY_LIGHT.powerOn) { 106 | console.log("Are we on? Yes."); 107 | callback(err, true); 108 | } 109 | else { 110 | console.log("Are we on? No."); 111 | callback(err, false); 112 | } 113 | }); 114 | 115 | // also add an "optional" Characteristic for Brightness 116 | light 117 | .getService(Service.Lightbulb) 118 | .addCharacteristic(Characteristic.Brightness) 119 | .on('get', function(callback) { 120 | callback(null, NURSERY_LIGHT.brightness); 121 | }) 122 | .on('set', function(value, callback) { 123 | NURSERY_LIGHT.setBrightness(value); 124 | callback(); 125 | }) 126 | 127 | light 128 | .getService(Service.Lightbulb) 129 | .addCharacteristic(Characteristic.Hue) 130 | .on('get',function(callback){ 131 | callback(null,NURSERY_LIGHT.hue); 132 | }) 133 | .on('set',function(value,callback){ 134 | NURSERY_LIGHT.setHue(value); 135 | callback(); 136 | }) 137 | 138 | light 139 | .getService(Service.Lightbulb) 140 | .addCharacteristic(Characteristic.Saturation) 141 | .on('get',function(callback){ 142 | callback(null,NURSERY_LIGHT.saturation); 143 | }) 144 | .on('set',function(value,callback){ 145 | NURSERY_LIGHT.setSaturation(value); 146 | callback(); 147 | }) 148 | -------------------------------------------------------------------------------- /accessories/NurseryTemperatureSensor_accessory.js: -------------------------------------------------------------------------------- 1 | var NurseryTemperature = 0.0; 2 | 3 | // MQTT Setup 4 | var mqtt = require('mqtt'); 5 | console.log("Connecting to MQTT broker..."); 6 | var mqtt = require('mqtt'); 7 | var options = { 8 | port: 1883, 9 | host: '192.168.1.12', 10 | clientId: 'AdyPi_NurseryTemperatureSensor' 11 | }; 12 | var client = mqtt.connect(options); 13 | console.log("Nursery Temperature Sensor Connected to MQTT broker"); 14 | client.subscribe('NurseryTemperature'); 15 | client.on('message', function(topic, message) { 16 | console.log(parseFloat(message)); 17 | NurseryTemperature = parseFloat(message); 18 | }); 19 | 20 | var Accessory = require('../').Accessory; 21 | var Service = require('../').Service; 22 | var Characteristic = require('../').Characteristic; 23 | var uuid = require('../').uuid; 24 | 25 | // here's a fake temperature sensor device that we'll expose to HomeKit 26 | var NURSERY_TEMP_SENSOR = { 27 | 28 | getTemperature: function() { 29 | console.log("Getting the current temperature!"); 30 | return parseFloat(NurseryTemperature); 31 | }, 32 | randomizeTemperature: function() { 33 | // randomize temperature to a value between 0 and 100 34 | NURSERY_TEMP_SENSOR.currentTemperature = parseFloat(NurseryTemperature);; 35 | } 36 | } 37 | 38 | 39 | // Generate a consistent UUID for our Temperature Sensor Accessory that will remain the same 40 | // even when restarting our server. We use the `uuid.generate` helper function to create 41 | // a deterministic UUID based on an arbitrary "namespace" and the string "temperature-sensor". 42 | var sensorUUID = uuid.generate('hap-nodejs:accessories:nursery-temperature-sensor'); 43 | 44 | // This is the Accessory that we'll return to HAP-NodeJS that represents our fake lock. 45 | var sensor = exports.accessory = new Accessory('Nursery Temperature Sensor', sensorUUID); 46 | 47 | // Add properties for publishing (in case we're using Core.js and not BridgedCore.js) 48 | sensor.username = "C3:5D:3A:AE:5E:FA"; 49 | sensor.pincode = "031-45-154"; 50 | 51 | // Add the actual TemperatureSensor Service. 52 | // We can see the complete list of Services and Characteristics in `lib/gen/HomeKitTypes.js` 53 | sensor 54 | .addService(Service.TemperatureSensor, "Nursery") 55 | .getCharacteristic(Characteristic.CurrentTemperature) 56 | .on('get', function(callback) { 57 | 58 | // return our current value 59 | callback(null, NURSERY_TEMP_SENSOR.getTemperature()); 60 | }); 61 | 62 | // randomize our temperature reading every 3 seconds 63 | setInterval(function() { 64 | 65 | NURSERY_TEMP_SENSOR.randomizeTemperature(); 66 | 67 | // update the characteristic value so interested iOS devices can get notified 68 | sensor 69 | .getService(Service.TemperatureSensor) 70 | .setCharacteristic(Characteristic.CurrentTemperature, NURSERY_TEMP_SENSOR.currentTemperature); 71 | 72 | }, 3000); 73 | -------------------------------------------------------------------------------- /accessories/types.js: -------------------------------------------------------------------------------- 1 | var exports = module.exports = {}; 2 | 3 | //HomeKit Types UUID's 4 | 5 | var stPre = "000000"; 6 | var stPost = "-0000-1000-8000-0026BB765291"; 7 | 8 | 9 | //HomeKitTransportCategoryTypes 10 | exports.OTHER_TCTYPE = 1; 11 | exports.FAN_TCTYPE = 3; 12 | exports.GARAGE_DOOR_OPENER_TCTYPE = 4; 13 | exports.LIGHTBULB_TCTYPE = 5; 14 | exports.DOOR_LOCK_TCTYPE = 6; 15 | exports.OUTLET_TCTYPE = 7; 16 | exports.SWITCH_TCTYPE = 8; 17 | exports.THERMOSTAT_TCTYPE = 9; 18 | exports.SENSOR_TCTYPE = 10; 19 | exports.ALARM_SYSTEM_TCTYPE = 11; 20 | exports.DOOR_TCTYPE = 12; 21 | exports.WINDOW_TCTYPE = 13; 22 | exports.WINDOW_COVERING_TCTYPE = 14; 23 | exports.PROGRAMMABLE_SWITCH_TCTYPE = 15; 24 | 25 | //HomeKitServiceTypes 26 | 27 | exports.LIGHTBULB_STYPE = stPre + "43" + stPost; 28 | exports.SWITCH_STYPE = stPre + "49" + stPost; 29 | exports.THERMOSTAT_STYPE = stPre + "4A" + stPost; 30 | exports.GARAGE_DOOR_OPENER_STYPE = stPre + "41" + stPost; 31 | exports.ACCESSORY_INFORMATION_STYPE = stPre + "3E" + stPost; 32 | exports.FAN_STYPE = stPre + "40" + stPost; 33 | exports.OUTLET_STYPE = stPre + "47" + stPost; 34 | exports.LOCK_MECHANISM_STYPE = stPre + "45" + stPost; 35 | exports.LOCK_MANAGEMENT_STYPE = stPre + "44" + stPost; 36 | exports.ALARM_STYPE = stPre + "7E" + stPost; 37 | exports.WINDOW_COVERING_STYPE = stPre + "8C" + stPost; 38 | exports.OCCUPANCY_SENSOR_STYPE = stPre + "86" + stPost; 39 | exports.CONTACT_SENSOR_STYPE = stPre + "80" + stPost; 40 | exports.MOTION_SENSOR_STYPE = stPre + "85" + stPost; 41 | exports.HUMIDITY_SENSOR_STYPE = stPre + "82" + stPost; 42 | exports.TEMPERATURE_SENSOR_STYPE = stPre + "8A" + stPost; 43 | 44 | //HomeKitCharacteristicsTypes 45 | 46 | 47 | exports.ALARM_CURRENT_STATE_CTYPE = stPre + "66" + stPost; 48 | exports.ALARM_TARGET_STATE_CTYPE = stPre + "67" + stPost; 49 | exports.ADMIN_ONLY_ACCESS_CTYPE = stPre + "01" + stPost; 50 | exports.AUDIO_FEEDBACK_CTYPE = stPre + "05" + stPost; 51 | exports.BRIGHTNESS_CTYPE = stPre + "08" + stPost; 52 | exports.BATTERY_LEVEL_CTYPE = stPre + "68" + stPost; 53 | exports.COOLING_THRESHOLD_CTYPE = stPre + "0D" + stPost; 54 | exports.CONTACT_SENSOR_STATE_CTYPE = stPre + "6A" + stPost; 55 | exports.CURRENT_DOOR_STATE_CTYPE = stPre + "0E" + stPost; 56 | exports.CURRENT_LOCK_MECHANISM_STATE_CTYPE = stPre + "1D" + stPost; 57 | exports.CURRENT_RELATIVE_HUMIDITY_CTYPE = stPre + "10" + stPost; 58 | exports.CURRENT_TEMPERATURE_CTYPE = stPre + "11" + stPost; 59 | exports.HEATING_THRESHOLD_CTYPE = stPre + "12" + stPost; 60 | exports.HUE_CTYPE = stPre + "13" + stPost; 61 | exports.IDENTIFY_CTYPE = stPre + "14" + stPost; 62 | exports.LOCK_MANAGEMENT_AUTO_SECURE_TIMEOUT_CTYPE = stPre + "1A" + stPost; 63 | exports.LOCK_MANAGEMENT_CONTROL_POINT_CTYPE = stPre + "19" + stPost; 64 | exports.LOCK_MECHANISM_LAST_KNOWN_ACTION_CTYPE = stPre + "1C" + stPost; 65 | exports.LOGS_CTYPE = stPre + "1F" + stPost; 66 | exports.MANUFACTURER_CTYPE = stPre + "20" + stPost; 67 | exports.MODEL_CTYPE = stPre + "21" + stPost; 68 | exports.MOTION_DETECTED_CTYPE = stPre + "22" + stPost; 69 | exports.NAME_CTYPE = stPre + "23" + stPost; 70 | exports.OBSTRUCTION_DETECTED_CTYPE = stPre + "24" + stPost; 71 | exports.OUTLET_IN_USE_CTYPE = stPre + "26" + stPost; 72 | exports.OCCUPANCY_DETECTED_CTYPE = stPre + "71" + stPost; 73 | exports.POWER_STATE_CTYPE = stPre + "25" + stPost; 74 | exports.PROGRAMMABLE_SWITCH_SWITCH_EVENT_CTYPE = stPre + "73" + stPost; 75 | exports.PROGRAMMABLE_SWITCH_OUTPUT_STATE_CTYPE = stPre + "74" + stPost; 76 | exports.ROTATION_DIRECTION_CTYPE = stPre + "28" + stPost; 77 | exports.ROTATION_SPEED_CTYPE = stPre + "29" + stPost; 78 | exports.SATURATION_CTYPE = stPre + "2F" + stPost; 79 | exports.SERIAL_NUMBER_CTYPE = stPre + "30" + stPost; 80 | exports.STATUS_LOW_BATTERY_CTYPE = stPre + "79" + stPost; 81 | exports.STATUS_FAULT_CTYPE = stPre + "77" + stPost; 82 | exports.TARGET_DOORSTATE_CTYPE = stPre + "32" + stPost; 83 | exports.TARGET_LOCK_MECHANISM_STATE_CTYPE = stPre + "1E" + stPost; 84 | exports.TARGET_RELATIVE_HUMIDITY_CTYPE = stPre + "34" + stPost; 85 | exports.TARGET_TEMPERATURE_CTYPE = stPre + "35" + stPost; 86 | exports.TEMPERATURE_UNITS_CTYPE = stPre + "36" + stPost; 87 | exports.VERSION_CTYPE = stPre + "37" + stPost; 88 | exports.WINDOW_COVERING_TARGET_POSITION_CTYPE = stPre + "7C" + stPost; 89 | exports.WINDOW_COVERING_CURRENT_POSITION_CTYPE = stPre + "6D" + stPost; 90 | exports.WINDOW_COVERING_OPERATION_STATE_CTYPE = stPre + "72" + stPost; 91 | exports.CURRENTHEATINGCOOLING_CTYPE = stPre + "0F" + stPost; 92 | exports.TARGETHEATINGCOOLING_CTYPE = stPre + "33" + stPost; 93 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Accessory = require('./lib/Accessory.js').Accessory; 2 | var Bridge = require('./lib/Bridge.js').Bridge; 3 | var Service = require('./lib/Service.js').Service; 4 | var Characteristic = require('./lib/Characteristic.js').Characteristic; 5 | var uuid = require('./lib/util/uuid'); 6 | var AccessoryLoader = require('./lib/AccessoryLoader.js'); 7 | var storage = require('node-persist'); 8 | 9 | // ensure Characteristic subclasses are defined 10 | var HomeKitTypes = require('./lib/gen/HomeKitTypes'); 11 | 12 | module.exports = { 13 | init: init, 14 | Accessory: Accessory, 15 | Bridge: Bridge, 16 | Service: Service, 17 | Characteristic: Characteristic, 18 | uuid: uuid, 19 | AccessoryLoader: AccessoryLoader 20 | } 21 | 22 | function init(storagePath) { 23 | // initialize our underlying storage system, passing on the directory if needed 24 | if (typeof storagePath !== 'undefined') 25 | storage.initSync({ dir: storagePath }); 26 | else 27 | storage.initSync(); // use whatever is default 28 | } -------------------------------------------------------------------------------- /lib/Accessory.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('Accessory'); 2 | var crypto = require('crypto') 3 | var inherits = require('util').inherits; 4 | var EventEmitter = require('events').EventEmitter; 5 | var clone = require('./util/clone').clone; 6 | var uuid = require('./util/uuid'); 7 | var Service = require('./Service').Service; 8 | var Characteristic = require('./Characteristic').Characteristic; 9 | var HomeKitTypes = require('./gen/HomeKitTypes'); 10 | var Advertiser = require('./Advertiser').Advertiser; 11 | var HAPServer = require('./HAPServer').HAPServer; 12 | var AccessoryInfo = require('./model/AccessoryInfo').AccessoryInfo; 13 | var IdentifierCache = require('./model/IdentifierCache').IdentifierCache; 14 | // var RelayServer = require("./util/relayserver").RelayServer; 15 | 16 | 'use strict'; 17 | 18 | module.exports = { 19 | Accessory: Accessory 20 | }; 21 | 22 | 23 | /** 24 | * Accessory is a virtual HomeKit device. It can publish an associated HAP server for iOS devices to communicate 25 | * with - or it can run behind another "Bridge" Accessory server. 26 | * 27 | * Bridged Accessories in this implementation must have a UUID that is unique among all other Accessories that 28 | * are hosted by the Bridge. This UUID must be "stable" and unchanging, even when the server is restarted. This 29 | * is required so that the Bridge can provide consistent "Accessory IDs" (aid) and "Instance IDs" (iid) for all 30 | * Accessories, Services, and Characteristics for iOS clients to reference later. 31 | * 32 | * @event 'identify' => function(paired, callback(err)) { } 33 | * Emitted when an iOS device wishes for this Accessory to identify itself. If `paired` is false, then 34 | * this device is currently browsing for Accessories in the system-provided "Add Accessory" screen. If 35 | * `paired` is true, then this is a device that has already paired with us. Note that if `paired` is true, 36 | * listening for this event is a shortcut for the underlying mechanism of setting the `Identify` Characteristic: 37 | * `getService(Service.AccessoryInformation).getCharacteristic(Characteristic.Identify).on('set', ...)` 38 | * You must call the callback for identification to be successful. 39 | * 40 | * @event 'service-characteristic-change' => function({service, characteristic, oldValue, newValue, context}) { } 41 | * Emitted after a change in the value of one of the provided Service's Characteristics. 42 | */ 43 | 44 | function Accessory(displayName, UUID) { 45 | 46 | if (!displayName) throw new Error("Accessories must be created with a non-empty displayName."); 47 | if (!UUID) throw new Error("Accessories must be created with a valid UUID."); 48 | if (!uuid.isValid(UUID)) throw new Error("UUID '" + UUID + "' is not a valid UUID. Try using the provided 'generateUUID' function to create a valid UUID from any arbitrary string, like a serial number."); 49 | 50 | this.displayName = displayName; 51 | this.UUID = UUID; 52 | this.aid = null; // assigned by us in assignIDs() or by a Bridge 53 | this._isBridge = false; // true if we are a Bridge (creating a new instance of the Bridge subclass sets this to true) 54 | this.bridged = false; // true if we are hosted "behind" a Bridge Accessory 55 | this.bridgedAccessories = []; // If we are a Bridge, these are the Accessories we are bridging 56 | this.reachable = true; 57 | this.category = Accessory.Categories.OTHER; 58 | this.services = []; // of Service 59 | 60 | // create our initial "Accessory Information" Service that all Accessories are expected to have 61 | this 62 | .addService(Service.AccessoryInformation) 63 | .setCharacteristic(Characteristic.Name, displayName) 64 | .setCharacteristic(Characteristic.Manufacturer, "Default-Manufacturer") 65 | .setCharacteristic(Characteristic.Model, "Default-Model") 66 | .setCharacteristic(Characteristic.SerialNumber, "Default-SerialNumber"); 67 | 68 | // sign up for when iOS attempts to "set" the Identify characteristic - this means a paired device wishes 69 | // for us to identify ourselves (as opposed to an unpaired device - that case is handled by HAPServer 'identify' event) 70 | this 71 | .getService(Service.AccessoryInformation) 72 | .getCharacteristic(Characteristic.Identify) 73 | .on('set', function(value, callback) { 74 | if (value) { 75 | var paired = true; 76 | this._identificationRequest(paired, callback); 77 | } 78 | }.bind(this)); 79 | } 80 | 81 | inherits(Accessory, EventEmitter); 82 | 83 | // Known category values. Category is a hint to iOS clients about what "type" of Accessory this represents, for UI only. 84 | Accessory.Categories = { 85 | OTHER: 1, 86 | BRIDGE: 2, 87 | FAN: 3, 88 | GARAGE_DOOR_OPENER: 4, 89 | LIGHTBULB: 5, 90 | DOOR_LOCK: 6, 91 | OUTLET: 7, 92 | SWITCH: 8, 93 | THERMOSTAT: 9, 94 | SENSOR: 10, 95 | ALARM_SYSTEM: 11, 96 | DOOR: 12, 97 | WINDOW: 13, 98 | WINDOW_COVERING: 14, 99 | PROGRAMMABLE_SWITCH: 15 100 | } 101 | 102 | Accessory.prototype._identificationRequest = function(paired, callback) { 103 | debug("[%s] Identification request", this.displayName); 104 | 105 | if (this.listeners('identify').length > 0) { 106 | // allow implementors to identify this Accessory in whatever way is appropriate, and pass along 107 | // the standard callback for completion. 108 | this.emit('identify', paired, callback); 109 | } 110 | else { 111 | debug("[%s] Identification request ignored; no listeners to 'identify' event", this.displayName); 112 | callback(); 113 | } 114 | } 115 | 116 | Accessory.prototype.addService = function(service) { 117 | // service might be a constructor like `Service.AccessoryInformation` instead of an instance 118 | // of Service. Coerce if necessary. 119 | if (typeof service === 'function') 120 | service = new (Function.prototype.bind.apply(service, arguments)); 121 | 122 | // check for UUID+subtype conflict 123 | for (var index in this.services) { 124 | var existing = this.services[index]; 125 | if (existing.UUID === service.UUID) { 126 | // OK we have two Services with the same UUID. Check that each defines a `subtype` property and that each is unique. 127 | if (!service.subtype) 128 | throw new Error("Cannot add a Service with the same UUID '" + existing.UUID + "' as another Service in this Accessory without also defining a unique 'subtype' property."); 129 | 130 | if (service.subtype.toString() === existing.subtype.toString()) 131 | throw new Error("Cannot add a Service with the same UUID '" + existing.UUID + "' and subtype '" + existing.subtype + "' as another Service in this Accessory."); 132 | } 133 | } 134 | 135 | if (!this.bridged) { 136 | this._updateConfiguration(); 137 | } else { 138 | this.emit('service-configurationChange', clone({accessory:this, service:service})); 139 | } 140 | 141 | service.on('service-configurationChange', function(change) { 142 | if (!this.bridged) { 143 | this._updateConfiguration(); 144 | } else { 145 | this.emit('service-configurationChange', clone({accessory:this, service:service})); 146 | } 147 | }.bind(this)); 148 | 149 | // listen for changes in characteristics and bubble them up 150 | service.on('characteristic-change', function(change) { 151 | this.emit('service-characteristic-change', clone(change, {service:service})); 152 | 153 | // if we're not bridged, when we'll want to process this event through our HAPServer 154 | if (!this.bridged) 155 | this._handleCharacteristicChange(clone(change, {accessory:this, service:service})); 156 | 157 | }.bind(this)); 158 | 159 | this.services.push(service); 160 | return service; 161 | } 162 | 163 | Accessory.prototype.removeService = function(service) { 164 | var targetServiceIndex; 165 | 166 | for (var index in this.services) { 167 | var existingService = this.services[index]; 168 | 169 | if (existingService === service) { 170 | targetServiceIndex = index; 171 | break; 172 | } 173 | } 174 | 175 | if (targetServiceIndex) { 176 | this.services.splice(targetServiceIndex, 1); 177 | 178 | if (!this.bridged) { 179 | this._updateConfiguration(); 180 | } else { 181 | this.emit('service-configurationChange', clone({accessory:this, service:service})); 182 | } 183 | 184 | service.removeAllListeners(); 185 | } 186 | } 187 | 188 | Accessory.prototype.getService = function(name) { 189 | for (var index in this.services) { 190 | var service = this.services[index]; 191 | 192 | if (typeof name === 'string' && (service.displayName === name || service.name === name)) 193 | return service; 194 | else if (typeof name === 'function' && ((service instanceof name) || (name.UUID === service.UUID))) 195 | return service; 196 | } 197 | } 198 | 199 | Accessory.prototype.updateReachability = function(reachable) { 200 | if (!this.bridged) 201 | throw new Error("Cannot update reachability on non-bridged accessory!"); 202 | this.reachable = reachable; 203 | 204 | this 205 | .getService(Service.BridgingState) 206 | .getCharacteristic(Characteristic.Reachable) 207 | .setValue((reachable == true)); 208 | } 209 | 210 | Accessory.prototype.addBridgedAccessory = function(accessory, deferUpdate) { 211 | if (accessory._isBridge) 212 | throw new Error("Cannot Bridge another Bridge!"); 213 | 214 | // check for UUID conflict 215 | for (var index in this.bridgedAccessories) { 216 | var existing = this.bridgedAccessories[index]; 217 | if (existing.UUID === accessory.UUID) 218 | throw new Error("Cannot add a bridged Accessory with the same UUID as another bridged Accessory: " + existing.UUID); 219 | } 220 | 221 | if(accessory.getService(Service.BridgingState) == undefined) { 222 | // Setup Bridging State Service 223 | accessory.addService(Service.BridgingState); 224 | } 225 | 226 | accessory 227 | .getService(Service.BridgingState) 228 | .getCharacteristic(Characteristic.AccessoryIdentifier) 229 | .setValue(accessory.UUID); 230 | 231 | accessory 232 | .getService(Service.BridgingState) 233 | .getCharacteristic(Characteristic.Reachable) 234 | .setValue(accessory.reachable); 235 | 236 | accessory 237 | .getService(Service.BridgingState) 238 | .getCharacteristic(Characteristic.Category) 239 | .setValue(accessory.category); 240 | 241 | // listen for changes in ANY characteristics of ANY services on this Accessory 242 | accessory.on('service-characteristic-change', function(change) { 243 | this._handleCharacteristicChange(clone(change, {accessory:accessory})); 244 | }.bind(this)); 245 | 246 | accessory.on('service-configurationChange', function(change) { 247 | this._updateConfiguration(); 248 | }.bind(this)); 249 | 250 | accessory.bridged = true; 251 | 252 | this.bridgedAccessories.push(accessory); 253 | 254 | if(!deferUpdate) { 255 | this._updateConfiguration(); 256 | } 257 | 258 | return accessory; 259 | } 260 | 261 | Accessory.prototype.addBridgedAccessories = function(accessories) { 262 | for (var index in accessories) { 263 | var accessory = accessories[index]; 264 | this.addBridgedAccessory(accessory, true); 265 | } 266 | 267 | this._updateConfiguration(); 268 | } 269 | 270 | Accessory.prototype.removeBridgedAccessory = function(accessory, deferUpdate) { 271 | if (accessory._isBridge) 272 | throw new Error("Cannot Bridge another Bridge!"); 273 | 274 | var foundMatchAccessory = false; 275 | // check for UUID conflict 276 | for (var index in this.bridgedAccessories) { 277 | var existing = this.bridgedAccessories[index]; 278 | if (existing.UUID === accessory.UUID) { 279 | foundMatchAccessory = true; 280 | this.bridgedAccessories.splice(index, 1); 281 | break; 282 | } 283 | } 284 | 285 | if (!foundMatchAccessory) 286 | throw new Error("Cannot find the bridged Accessory to remove."); 287 | 288 | accessory.removeAllListeners(); 289 | 290 | if(!deferUpdate) { 291 | this._updateConfiguration(); 292 | } 293 | } 294 | 295 | Accessory.prototype.removeBridgedAccessories = function(accessories) { 296 | for (var index in accessories) { 297 | var accessory = accessories[index]; 298 | this.removeBridgedAccessory(accessory, true); 299 | } 300 | 301 | this._updateConfiguration(); 302 | } 303 | 304 | Accessory.prototype.getCharacteristicByIID = function(iid) { 305 | for (var index in this.services) { 306 | var service = this.services[index]; 307 | var characteristic = service.getCharacteristicByIID(iid); 308 | if (characteristic) return characteristic; 309 | } 310 | } 311 | 312 | Accessory.prototype.getBridgedAccessoryByAID = function(aid) { 313 | for (var index in this.bridgedAccessories) { 314 | var accessory = this.bridgedAccessories[index]; 315 | if (accessory.aid === aid) return accessory; 316 | } 317 | } 318 | 319 | Accessory.prototype.findCharacteristic = function(aid, iid) { 320 | 321 | // if aid === 1, the accessory is us (because we are the server), otherwise find it among our bridged 322 | // accessories (if any) 323 | var accessory = (aid === 1) ? this : this.getBridgedAccessoryByAID(aid); 324 | 325 | return accessory && accessory.getCharacteristicByIID(iid); 326 | } 327 | 328 | /** 329 | * Assigns aid/iid to ourselves, any Accessories we are bridging, and all associated Services+Characteristics. Uses 330 | * the provided identifierCache to keep IDs stable. 331 | */ 332 | Accessory.prototype._assignIDs = function(identifierCache) { 333 | 334 | // if we are responsible for our own identifierCache, start the expiration process 335 | if (this._identifierCache) { 336 | this._identifierCache.startTrackingUsage(); 337 | } 338 | 339 | if (this.bridged) { 340 | // This Accessory is bridged, so it must have an aid > 1. Use the provided identifierCache to 341 | // fetch or assign one based on our UUID. 342 | this.aid = identifierCache.getAID(this.UUID) 343 | } 344 | else { 345 | // Since this Accessory is the server (as opposed to any Accessories that may be bridged behind us), 346 | // we must have aid = 1 347 | this.aid = 1; 348 | } 349 | 350 | for (var index in this.services) { 351 | var service = this.services[index]; 352 | service._assignIDs(identifierCache, this.UUID); 353 | } 354 | 355 | // now assign IDs for any Accessories we are bridging 356 | for (var index in this.bridgedAccessories) { 357 | var accessory = this.bridgedAccessories[index]; 358 | 359 | accessory._assignIDs(identifierCache); 360 | } 361 | 362 | // expire any now-unused cache keys (for Accessories, Services, or Characteristics 363 | // that have been removed since the last call to assignIDs()) 364 | if (this._identifierCache) { 365 | this._identifierCache.stopTrackingUsageAndExpireUnused(); 366 | this._identifierCache.save(); 367 | } 368 | } 369 | 370 | /** 371 | * Returns a JSON representation of this Accessory suitable for delivering to HAP clients. 372 | */ 373 | Accessory.prototype.toHAP = function(opt) { 374 | 375 | var servicesHAP = []; 376 | 377 | for (var index in this.services) { 378 | var service = this.services[index]; 379 | servicesHAP.push(service.toHAP(opt)); 380 | } 381 | 382 | var accessoriesHAP = [{ 383 | aid: this.aid, 384 | services: servicesHAP 385 | }]; 386 | 387 | // now add any Accessories we are bridging 388 | for (var index in this.bridgedAccessories) { 389 | var accessory = this.bridgedAccessories[index]; 390 | var bridgedAccessoryHAP = accessory.toHAP(opt); 391 | 392 | // bridgedAccessoryHAP is an array of accessories with one item - extract it 393 | // and add it to our own array 394 | accessoriesHAP.push(bridgedAccessoryHAP[0]) 395 | } 396 | 397 | return accessoriesHAP; 398 | } 399 | 400 | /** 401 | * Publishes this Accessory on the local network for iOS clients to communicate with. 402 | * 403 | * @param {Object} info - Required info for publishing. 404 | * @param {string} info.username - The "username" (formatted as a MAC address - like "CC:22:3D:E3:CE:F6") of 405 | * this Accessory. Must be globally unique from all Accessories on your local network. 406 | * @param {number} info.port - The port to run the HAP server on for clients to use. 407 | * @param {string} info.pincode - The 8-digit pincode for clients to use when pairing this Accessory. Must be formatted 408 | * as a string like "031-45-154". 409 | * @param {string} info.category - One of the values of the Accessory.Category enum, like Accessory.Category.SWITCH. 410 | * This is a hint to iOS clients about what "type" of Accessory this represents, so 411 | * that for instance an appropriate icon can be drawn for the user while adding a 412 | * new Accessory. 413 | */ 414 | Accessory.prototype.publish = function(info, allowInsecureRequest) { 415 | // attempt to load existing AccessoryInfo from disk 416 | this._accessoryInfo = AccessoryInfo.load(info.username); 417 | 418 | // if we don't have one, create a new one. 419 | if (!this._accessoryInfo) { 420 | debug("[%s] Creating new AccessoryInfo for our HAP server", this.displayName); 421 | this._accessoryInfo = AccessoryInfo.create(info.username); 422 | } 423 | 424 | // make sure we have up-to-date values in AccessoryInfo, then save it in case they changed (or if we just created it) 425 | this._accessoryInfo.displayName = this.displayName; 426 | this._accessoryInfo.port = info.port; 427 | this._accessoryInfo.category = info.category || Accessory.Categories.OTHER; 428 | this._accessoryInfo.pincode = info.pincode; 429 | this._accessoryInfo.save(); 430 | 431 | // if (this._isBridge) { 432 | // this.relayServer = new RelayServer(this._accessoryInfo); 433 | // this.addService(this.relayServer.relayService()); 434 | // } 435 | 436 | // create our IdentifierCache so we can provide clients with stable aid/iid's 437 | this._identifierCache = IdentifierCache.load(info.username); 438 | 439 | // if we don't have one, create a new one. 440 | if (!this._identifierCache) { 441 | debug("[%s] Creating new IdentifierCache", this.displayName); 442 | this._identifierCache = new IdentifierCache(info.username); 443 | } 444 | 445 | // assign aid/iid 446 | this._assignIDs(this._identifierCache); 447 | 448 | // get our accessory information in HAP format and determine if our configuration (that is, our 449 | // Accessories/Services/Characteristics) has changed since the last time we were published. make 450 | // sure to omit actual values since these are not part of the "configuration". 451 | var config = this.toHAP({omitValues:true}); 452 | 453 | // now convert it into a hash code and check it against the last one we made, if we have one 454 | var shasum = crypto.createHash('sha1'); 455 | shasum.update(JSON.stringify(config)); 456 | var configHash = shasum.digest('hex'); 457 | 458 | if (configHash !== this._accessoryInfo.configHash) { 459 | 460 | // our configuration has changed! we'll need to bump our config version number 461 | this._accessoryInfo.configVersion++; 462 | this._accessoryInfo.configHash = configHash; 463 | this._accessoryInfo.save(); 464 | } 465 | 466 | // create our Advertiser which broadcasts our presence over mdns 467 | this._advertiser = new Advertiser(this._accessoryInfo); 468 | 469 | // create our HAP server which handles all communication between iOS devices and us 470 | this._server = new HAPServer(this._accessoryInfo, this.relayServer); 471 | this._server.allowInsecureRequest = allowInsecureRequest 472 | this._server.on('listening', this._onListening.bind(this)); 473 | this._server.on('identify', this._handleIdentify.bind(this)); 474 | this._server.on('pair', this._handlePair.bind(this)); 475 | this._server.on('unpair', this._handleUnpair.bind(this)); 476 | this._server.on('accessories', this._handleAccessories.bind(this)); 477 | this._server.on('get-characteristics', this._handleGetCharacteristics.bind(this)); 478 | this._server.on('set-characteristics', this._handleSetCharacteristics.bind(this)); 479 | this._server.listen(this._accessoryInfo.port); 480 | } 481 | 482 | Accessory.prototype._updateConfiguration = function() { 483 | if (this._advertiser && this._advertiser.isAdvertising()) { 484 | // get our accessory information in HAP format and determine if our configuration (that is, our 485 | // Accessories/Services/Characteristics) has changed since the last time we were published. make 486 | // sure to omit actual values since these are not part of the "configuration". 487 | var config = this.toHAP({omitValues:true}); 488 | 489 | // now convert it into a hash code and check it against the last one we made, if we have one 490 | var shasum = crypto.createHash('sha1'); 491 | shasum.update(JSON.stringify(config)); 492 | var configHash = shasum.digest('hex'); 493 | 494 | if (configHash !== this._accessoryInfo.configHash) { 495 | 496 | // our configuration has changed! we'll need to bump our config version number 497 | this._accessoryInfo.configVersion++; 498 | this._accessoryInfo.configHash = configHash; 499 | this._accessoryInfo.save(); 500 | } 501 | 502 | // update our advertisement so HomeKit on iOS can pickup new accessory 503 | this._advertiser.updateAdvertisement(); 504 | } 505 | } 506 | 507 | Accessory.prototype._onListening = function() { 508 | // the HAP server is listening, so we can now start advertising our presence. 509 | this._advertiser.startAdvertising(); 510 | } 511 | 512 | // Called when an unpaired client wishes for us to identify ourself 513 | Accessory.prototype._handleIdentify = function(callback) { 514 | var paired = false; 515 | this._identificationRequest(paired, callback); 516 | } 517 | 518 | // Called when HAPServer has completed the pairing process with a client 519 | Accessory.prototype._handlePair = function(username, publicKey, callback) { 520 | 521 | debug("[%s] Paired with client %s", this.displayName, username); 522 | 523 | this._accessoryInfo.addPairedClient(username, publicKey); 524 | this._accessoryInfo.save(); 525 | 526 | // update our advertisement so it can pick up on the paired status of AccessoryInfo 527 | this._advertiser.updateAdvertisement(); 528 | 529 | callback(); 530 | } 531 | 532 | // Called when HAPServer wishes to remove/unpair the pairing information of a client 533 | Accessory.prototype._handleUnpair = function(username, callback) { 534 | 535 | debug("[%s] Unpairing with client %s", this.displayName, username); 536 | 537 | // Unpair 538 | this._accessoryInfo.removePairedClient(username); 539 | this._accessoryInfo.save(); 540 | 541 | // update our advertisement so it can pick up on the paired status of AccessoryInfo 542 | this._advertiser.updateAdvertisement(); 543 | 544 | callback(); 545 | } 546 | 547 | // Called when an iOS client wishes to know all about our accessory via JSON payload 548 | Accessory.prototype._handleAccessories = function(callback) { 549 | 550 | // make sure our aid/iid's are all assigned 551 | this._assignIDs(this._identifierCache); 552 | 553 | // build out our JSON payload and call the callback 554 | callback(null, { 555 | accessories: this.toHAP() // array of Accessory HAP 556 | }); 557 | } 558 | 559 | // Called when an iOS client wishes to query the state of one or more characteristics, like "door open?", "light on?", etc. 560 | Accessory.prototype._handleGetCharacteristics = function(data, events, callback, remote) { 561 | 562 | // build up our array of responses to the characteristics requested asynchronously 563 | var characteristics = []; 564 | var statusKey = remote ? 's' : 'status'; 565 | var valueKey = remote ? 'v' : 'value'; 566 | 567 | data.forEach(function(characteristicData) { 568 | var aid = characteristicData.aid; 569 | var iid = characteristicData.iid; 570 | 571 | var includeEvent = characteristicData.e; 572 | 573 | var characteristic = this.findCharacteristic(characteristicData.aid, characteristicData.iid); 574 | 575 | if (!characteristic) { 576 | debug('[%s] Could not find a Characteristic with iid of %s and aid of %s', this.displayName, characteristicData.aid, characteristicData.iid); 577 | var response = { 578 | aid: aid, 579 | iid: iid 580 | }; 581 | response[statusKey] = HAPServer.Status.SERVICE_COMMUNICATION_FAILURE; // generic error status 582 | characteristics.push(response); 583 | 584 | // have we collected all responses yet? 585 | if (characteristics.length === data.length) 586 | callback(null, characteristics); 587 | 588 | return; 589 | } 590 | 591 | // Found the Characteristic! Get the value! 592 | debug('[%s] Getting value for Characteristic "%s"', this.displayName, characteristic.displayName); 593 | 594 | // we want to remember "who" made this request, so that we don't send them an event notification 595 | // about any changes that occurred as a result of the request. For instance, if after querying 596 | // the current value of a characteristic, the value turns out to be different than the previously 597 | // cached Characteristic value, an internal 'change' event will be emitted which will cause us to 598 | // notify all connected clients about that new value. But this client is about to get the new value 599 | // anyway, so we don't want to notify it twice. 600 | var context = events; 601 | 602 | // set the value and wait for success 603 | characteristic.getValue(function(err, value) { 604 | 605 | debug('[%s] Got Characteristic "%s" value: %s', this.displayName, characteristic.displayName, value); 606 | 607 | if (err) { 608 | debug('[%s] Error getting value for Characteristic "%s": %s', this.displayName, characteristic.displayName, err.message); 609 | var response = { 610 | aid: aid, 611 | iid: iid 612 | }; 613 | response[statusKey] = HAPServer.Status.SERVICE_COMMUNICATION_FAILURE; // generic error status 614 | characteristics.push(response); 615 | } 616 | else { 617 | var response = { 618 | aid: aid, 619 | iid: iid 620 | }; 621 | response[valueKey] = value; 622 | response[statusKey] = 0; 623 | 624 | if (includeEvent) { 625 | var eventName = aid + '.' + iid; 626 | response['e'] = (events[eventName] === true); 627 | } 628 | 629 | // compose the response and add it to the list 630 | characteristics.push(response); 631 | } 632 | 633 | // have we collected all responses yet? 634 | if (characteristics.length === data.length) 635 | callback(null, characteristics); 636 | 637 | }.bind(this), context); 638 | 639 | }.bind(this)); 640 | } 641 | 642 | // Called when an iOS client wishes to change the state of this accessory - like opening a door, or turning on a light. 643 | // Or, to subscribe to change events for a particular Characteristic. 644 | Accessory.prototype._handleSetCharacteristics = function(data, events, callback, remote) { 645 | 646 | // data is an array of characteristics and values like this: 647 | // [ { aid: 1, iid: 8, value: true, ev: true } ] 648 | 649 | debug("[%s] Processing characteristic set: %s", this.displayName, JSON.stringify(data)); 650 | 651 | // build up our array of responses to the characteristics requested asynchronously 652 | var characteristics = []; 653 | 654 | data.forEach(function(characteristicData) { 655 | var aid = characteristicData.aid; 656 | var iid = characteristicData.iid; 657 | var value = remote ? characteristicData.v : characteristicData.value; 658 | var ev = remote ? characteristicData.e : characteristicData.ev; 659 | var includeValue = characteristicData.r || false; 660 | 661 | var statusKey = remote ? 's' : 'status'; 662 | 663 | var characteristic = this.findCharacteristic(aid, iid); 664 | 665 | if (!characteristic) { 666 | debug('[%s] Could not find a Characteristic with iid of %s and aid of %s', this.displayName, characteristicData.aid, characteristicData.iid); 667 | var response = { 668 | aid: aid, 669 | iid: iid 670 | }; 671 | response[statusKey] = HAPServer.Status.SERVICE_COMMUNICATION_FAILURE; // generic error status 672 | characteristics.push(response); 673 | 674 | // have we collected all responses yet? 675 | if (characteristics.length === data.length) 676 | callback(null, characteristics); 677 | 678 | return; 679 | } 680 | 681 | // we want to remember "who" initiated this change, so that we don't send them an event notification 682 | // about the change they just made. We do this by leveraging the arbitrary "context" object supported 683 | // by Characteristic and passed on to the corresponding 'change' events bubbled up from Characteristic 684 | // through Service and Accessory. We'll assign it to the events object since it essentially represents 685 | // the connection requesting the change. 686 | var context = events; 687 | 688 | // if "ev" is present, that means we need to register or unregister this client for change events for 689 | // this characteristic. 690 | if (typeof ev !== 'undefined') { 691 | debug('[%s] %s Characteristic "%s" for events', this.displayName, ev ? "Registering" : "Unregistering", characteristic.displayName); 692 | 693 | // store event registrations in the supplied "events" dict which is associated with the connection making 694 | // the request. 695 | var eventName = aid + '.' + iid; 696 | 697 | if (ev) 698 | events[eventName] = true; // value is arbitrary, just needs to be non-falsey 699 | else 700 | delete events[eventName]; // unsubscribe by deleting name from dict 701 | } 702 | 703 | // Found the characteristic - set the value if there is one 704 | if (typeof value !== 'undefined') { 705 | 706 | debug('[%s] Setting Characteristic "%s" to value %s', this.displayName, characteristic.displayName, value); 707 | 708 | // set the value and wait for success 709 | characteristic.setValue(value, function(err) { 710 | 711 | if (err) { 712 | debug('[%s] Error setting Characteristic "%s" to value %s: ', this.displayName, characteristic.displayName, value, err.message); 713 | 714 | var response = { 715 | aid: aid, 716 | iid: iid 717 | }; 718 | response[statusKey] = HAPServer.Status.SERVICE_COMMUNICATION_FAILURE; // generic error status 719 | characteristics.push(response); 720 | } 721 | else { 722 | var response = { 723 | aid: aid, 724 | iid: iid 725 | }; 726 | response[statusKey] = 0; 727 | 728 | if (includeValue) 729 | response['v'] = characteristic.value; 730 | 731 | characteristics.push(response); 732 | } 733 | 734 | // have we collected all responses yet? 735 | if (characteristics.length === data.length) 736 | callback(null, characteristics); 737 | 738 | }.bind(this), context); 739 | 740 | } 741 | else { 742 | // no value to set, so we're done (success) 743 | var response = { 744 | aid: aid, 745 | iid: iid 746 | }; 747 | response[statusKey] = 0; 748 | characteristics.push(response); 749 | 750 | // have we collected all responses yet? 751 | if (characteristics.length === data.length) 752 | callback(null, characteristics); 753 | } 754 | 755 | }.bind(this)); 756 | } 757 | 758 | // Called internally above when a change was detected in one of our hosted Characteristics somewhere in our hierarchy. 759 | Accessory.prototype._handleCharacteristicChange = function(change) { 760 | if (!this._server) 761 | return; // we're not running a HAPServer, so there's no one to notify about this event 762 | 763 | var data = { 764 | characteristics: [{ 765 | aid: change.accessory.aid, 766 | iid: change.characteristic.iid, 767 | value: change.newValue 768 | }] 769 | }; 770 | 771 | // name for this event that corresponds to what we stored when the client signed up (in handleSetCharacteristics) 772 | var eventName = change.accessory.aid + '.' + change.characteristic.iid; 773 | 774 | // pull the events object associated with the original connection (if any) that initiated the change request, 775 | // which we assigned in handleGetCharacteristics/handleSetCharacteristics. 776 | var excludeEvents = change.context; 777 | 778 | // pass it along to notifyClients() so that it can omit the connection where events === excludeEvents. 779 | this._server.notifyClients(eventName, data, excludeEvents); 780 | } 781 | 782 | Accessory.prototype._setupService = function(service) { 783 | service.on('service-configurationChange', function(change) { 784 | if (!this.bridged) { 785 | this._updateConfiguration(); 786 | } else { 787 | this.emit('service-configurationChange', clone({accessory:this, service:service})); 788 | } 789 | }.bind(this)); 790 | 791 | // listen for changes in characteristics and bubble them up 792 | service.on('characteristic-change', function(change) { 793 | this.emit('service-characteristic-change', clone(change, {service:service})); 794 | 795 | // if we're not bridged, when we'll want to process this event through our HAPServer 796 | if (!this.bridged) 797 | this._handleCharacteristicChange(clone(change, {accessory:this, service:service})); 798 | 799 | }.bind(this)); 800 | } 801 | 802 | Accessory.prototype._sideloadServices = function(targetServices) { 803 | for (var index in targetServices) { 804 | var target = targetServices[index]; 805 | this._setupService(target); 806 | } 807 | 808 | this.services = targetServices; 809 | 810 | // Fix Identify 811 | this 812 | .getService(Service.AccessoryInformation) 813 | .getCharacteristic(Characteristic.Identify) 814 | .on('set', function(value, callback) { 815 | if (value) { 816 | var paired = true; 817 | this._identificationRequest(paired, callback); 818 | } 819 | }.bind(this)); 820 | } -------------------------------------------------------------------------------- /lib/AccessoryLoader.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var Accessory = require('./Accessory').Accessory; 4 | var Service = require('./Service').Service; 5 | var Characteristic = require('./Characteristic').Characteristic; 6 | var uuid = require('./util/uuid'); 7 | var storage = require('node-persist'); 8 | 9 | module.exports = { 10 | loadDirectory: loadDirectory, 11 | parseAccessoryJSON: parseAccessoryJSON, 12 | parseServiceJSON: parseServiceJSON, 13 | parseCharacteristicJSON: parseCharacteristicJSON 14 | }; 15 | 16 | /** 17 | * Loads all accessories from the given folder. Handles object-literal-style accessories, "accessory factories", 18 | * and new-API style modules. 19 | */ 20 | 21 | function loadDirectory(dir) { 22 | 23 | // exported accessory objects loaded from this dir 24 | var accessories = []; 25 | 26 | fs.readdirSync(dir).forEach(function(file) { 27 | 28 | // "Accessories" are modules that export a single accessory. 29 | if (file.split('_').pop() === "accessory.js") { 30 | console.log("Parsing accessory: " + file); 31 | 32 | var loadedAccessory = require(path.join(dir, file)).accessory; 33 | accessories.push(loadedAccessory); 34 | } 35 | // "Accessory Factories" are modules that export an array of accessories. 36 | else if (file.split('_').pop() === "accfactory.js") { 37 | console.log("Parsing accessory factory: " + file); 38 | 39 | // should return an array of objects { accessory: accessory-json } 40 | var loadedAccessories = require(path.join(dir, file)); 41 | accessories = accessories.concat(loadedAccessories); 42 | } 43 | }); 44 | 45 | // now we need to coerce all accessory objects into instances of Accessory (some or all of them may 46 | // be object-literal JSON-style accessories) 47 | return accessories.map(function(accessory) { 48 | return (accessory instanceof Accessory) ? accessory : parseAccessoryJSON(accessory); 49 | }) 50 | } 51 | 52 | /** 53 | * Accepts object-literal JSON structures from previous versions of HAP-NodeJS and parses them into 54 | * newer-style structures of Accessory/Service/Characteristic objects. 55 | */ 56 | 57 | function parseAccessoryJSON(json) { 58 | 59 | // parse services first so we can extract the accessory name 60 | var services = []; 61 | 62 | json.services.forEach(function(serviceJSON) { 63 | var service = parseServiceJSON(serviceJSON); 64 | services.push(service); 65 | }); 66 | 67 | var displayName = json.displayName; 68 | 69 | services.forEach(function(service) { 70 | if (service.UUID == '0000003E-0000-1000-8000-0026BB765291') // Service.AccessoryInformation.UUID 71 | service.characteristics.forEach(function(characteristic) { 72 | if (characteristic.UUID == '00000023-0000-1000-8000-0026BB765291') // Characteristic.Name.UUID 73 | displayName = characteristic.value; 74 | }); 75 | }); 76 | 77 | var accessory = new Accessory(displayName, uuid.generate(displayName)); 78 | 79 | // create custom properties for "username" and "pincode" for Core.js to find later (if using Core.js) 80 | accessory.username = json.username; 81 | accessory.pincode = json.pincode; 82 | 83 | // clear out the default services 84 | accessory.services.length = 0; 85 | 86 | // add services 87 | services.forEach(function(service) { 88 | accessory.addService(service); 89 | }); 90 | 91 | return accessory; 92 | } 93 | 94 | function parseServiceJSON(json) { 95 | var serviceUUID = json.sType; 96 | 97 | // build characteristics first so we can extract the Name (if present) 98 | var characteristics = []; 99 | 100 | json.characteristics.forEach(function(characteristicJSON) { 101 | var characteristic = parseCharacteristicJSON(characteristicJSON); 102 | characteristics.push(characteristic); 103 | }) 104 | 105 | var displayName = null; 106 | 107 | // extract the "Name" characteristic to use for 'type' discrimination if necessary 108 | characteristics.forEach(function(characteristic) { 109 | if (characteristic.UUID == '00000023-0000-1000-8000-0026BB765291') // Characteristic.Name.UUID 110 | displayName = characteristic.value; 111 | }); 112 | 113 | // Use UUID for "displayName" if necessary, as the JSON structures don't have a value for this 114 | var service = new Service(displayName || serviceUUID, serviceUUID, displayName); 115 | 116 | characteristics.forEach(function(characteristic) { 117 | if (characteristic.UUID != '00000023-0000-1000-8000-0026BB765291') // Characteristic.Name.UUID, already present in all Services 118 | service.addCharacteristic(characteristic); 119 | }) 120 | 121 | return service; 122 | } 123 | 124 | function parseCharacteristicJSON(json) { 125 | var characteristicUUID = json.cType; 126 | 127 | var characteristic = new Characteristic(json.manfDescription || characteristicUUID, characteristicUUID); 128 | 129 | // copy simple properties 130 | characteristic.value = json.initialValue; 131 | characteristic.setProps({ 132 | format: json.format, // example: "int" 133 | minValue: json.designedMinValue, 134 | maxValue: json.designedMaxValue, 135 | minStep: json.designedMinStep, 136 | unit: json.unit, 137 | perms: json.perms // example: ["pw","pr","ev"] 138 | }); 139 | 140 | // monkey-patch this characteristic to add the legacy method `updateValue` which used to exist, 141 | // and that accessory modules had access to via the `onRegister` function. This was the old mechanism 142 | // for communicating state changes about accessories that happened "outside" HomeKit. 143 | characteristic.updateValue = function(value, peer) { 144 | characteristic.setValue(value); 145 | } 146 | 147 | // monkey-patch legacy "locals" property which used to exist. 148 | characteristic.locals = json.locals; 149 | 150 | var updateFunc = json.onUpdate; // optional function(value) 151 | var readFunc = json.onRead; // optional function(callback(value)) 152 | var registerFunc = json.onRegister; // optional function 153 | 154 | if (updateFunc) { 155 | characteristic.on('set', function(value, callback) { 156 | updateFunc(value); 157 | callback(); 158 | }); 159 | } 160 | 161 | if (readFunc) { 162 | characteristic.on('get', function(callback) { 163 | readFunc(function(value) { 164 | callback(null, value); // old onRead callbacks don't use Error as first param 165 | }); 166 | }); 167 | } 168 | 169 | if (registerFunc) { 170 | registerFunc(characteristic); 171 | } 172 | 173 | return characteristic; 174 | } -------------------------------------------------------------------------------- /lib/Advertiser.js: -------------------------------------------------------------------------------- 1 | var mdns = require('mdns'); 2 | 3 | 'use strict'; 4 | 5 | module.exports = { 6 | Advertiser: Advertiser 7 | }; 8 | 9 | 10 | /** 11 | * Advertiser uses mdns to broadcast the presence of an Accessory to the local network. 12 | * 13 | * Note that as of iOS 9, an accessory can only pair with a single client. Instead of pairing your 14 | * accessories with multiple iOS devices in your home, Apple intends for you to use Home Sharing. 15 | * To support this requirement, we provide the ability to be "discoverable" or not (via a "service flag" on the 16 | * mdns payload). 17 | */ 18 | 19 | function Advertiser(accessoryInfo, port) { 20 | this.accessoryInfo = accessoryInfo; 21 | this._advertisement = null; 22 | } 23 | 24 | Advertiser.prototype.startAdvertising = function() { 25 | 26 | // stop advertising if necessary 27 | if (this._advertisement) { 28 | this.stopAdvertising(); 29 | } 30 | 31 | var txtRecord = { 32 | md: this.accessoryInfo.displayName, 33 | pv: "1.0", 34 | id: this.accessoryInfo.username, 35 | "c#": this.accessoryInfo.configVersion + "", // "accessory conf" - represents the "configuration version" of an Accessory. Increasing this "version number" signals iOS devices to re-fetch /accessories data. 36 | "s#": "1", // "accessory state" 37 | "ff": "0", 38 | "ci": this.accessoryInfo.category, 39 | "sf": this.accessoryInfo.paired() ? "0" : "1" // "sf == 1" means "discoverable by HomeKit iOS clients" 40 | }; 41 | 42 | // create/recreate our advertisement 43 | this._advertisement = mdns.createAdvertisement(mdns.tcp('hap'), this.accessoryInfo.port, { 44 | name: this.accessoryInfo.displayName, 45 | txtRecord: txtRecord 46 | }); 47 | this._advertisement.start(); 48 | } 49 | 50 | Advertiser.prototype.isAdvertising = function() { 51 | return (this._advertisement != null); 52 | } 53 | 54 | Advertiser.prototype.updateAdvertisement = function() { 55 | if (this._advertisement) { 56 | 57 | var txtRecord = { 58 | md: this.accessoryInfo.displayName, 59 | pv: "1.0", 60 | id: this.accessoryInfo.username, 61 | "c#": this.accessoryInfo.configVersion + "", // "accessory conf" - represents the "configuration version" of an Accessory. Increasing this "version number" signals iOS devices to re-fetch /accessories data. 62 | "s#": "1", // "accessory state" 63 | "ff": "0", 64 | "ci": this.accessoryInfo.category, 65 | "sf": this.accessoryInfo.paired() ? "0" : "1" // "sf == 1" means "discoverable by HomeKit iOS clients" 66 | }; 67 | 68 | this._advertisement.updateTXTRecord(txtRecord); 69 | } 70 | } 71 | 72 | Advertiser.prototype.stopAdvertising = function() { 73 | if (this._advertisement) { 74 | this._advertisement.stop(); 75 | this._advertisement = null; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/Bridge.js: -------------------------------------------------------------------------------- 1 | var Accessory = require('./Accessory').Accessory; 2 | var inherits = require('util').inherits; 3 | 4 | module.exports = { 5 | Bridge: Bridge 6 | }; 7 | 8 | /** 9 | * Bridge is a special type of HomeKit Accessory that hosts other Accessories "behind" it. This way you 10 | * can simply publish() the Bridge (with a single HAPServer on a single port) and all bridged Accessories 11 | * will be hosted automatically, instead of needed to publish() every single Accessory as a separate server. 12 | */ 13 | 14 | function Bridge(displayName, serialNumber) { 15 | Accessory.call(this, displayName, serialNumber); 16 | this._isBridge = true; 17 | } 18 | 19 | inherits(Bridge, Accessory); 20 | -------------------------------------------------------------------------------- /lib/Characteristic.js: -------------------------------------------------------------------------------- 1 | var inherits = require('util').inherits; 2 | var EventEmitter = require('events').EventEmitter; 3 | var once = require('./util/once').once; 4 | 5 | 'use strict'; 6 | 7 | module.exports = { 8 | Characteristic: Characteristic 9 | } 10 | 11 | 12 | /** 13 | * Characteristic represents a particular typed variable that can be assigned to a Service. For instance, a 14 | * "Hue" Characteristic might store a 'float' value of type 'arcdegrees'. You could add the Hue Characteristic 15 | * to a Service in order to store that value. A particular Characteristic is distinguished from others by its 16 | * UUID. HomeKit provides a set of known Characteristic UUIDs defined in HomeKitTypes.js along with a 17 | * corresponding concrete subclass. 18 | * 19 | * You can also define custom Characteristics by providing your own UUID. Custom Characteristics can be added 20 | * to any native or custom Services, but Siri will likely not be able to work with these. 21 | * 22 | * Note that you can get the "value" of a Characteristic by accessing the "value" property directly, but this 23 | * is really a "cached value". If you want to fetch the latest value, which may involve doing some work, then 24 | * call getValue(). 25 | * 26 | * @event 'get' => function(callback(err, newValue), context) { } 27 | * Emitted when someone calls getValue() on this Characteristic and desires the latest non-cached 28 | * value. If there are any listeners to this event, one of them MUST call the callback in order 29 | * for the value to ever be delivered. The `context` object is whatever was passed in by the initiator 30 | * of this event (for instance whomever called `getValue`). 31 | * 32 | * @event 'set' => function(newValue, callback(err), context) { } 33 | * Emitted when someone calls setValue() on this Characteristic with a desired new value. If there 34 | * are any listeners to this event, one of them MUST call the callback in order for this.value to 35 | * actually be set. The `context` object is whatever was passed in by the initiator of this change 36 | * (for instance, whomever called `setValue`). 37 | * 38 | * @event 'change' => function({ oldValue, newValue, context }) { } 39 | * Emitted after a change in our value has occurred. The new value will also be immediately accessible 40 | * in this.value. The event object contains the new value as well as the context object originally 41 | * passed in by the initiator of this change (if known). 42 | */ 43 | 44 | function Characteristic(displayName, UUID, props) { 45 | this.displayName = displayName; 46 | this.UUID = UUID; 47 | this.iid = null; // assigned by our containing Service 48 | this.value = null; 49 | this.props = props || { 50 | format: null, 51 | unit: null, 52 | minValue: null, 53 | maxValue: null, 54 | minStep: null, 55 | perms: [] 56 | }; 57 | } 58 | 59 | inherits(Characteristic, EventEmitter); 60 | 61 | // Known HomeKit formats 62 | Characteristic.Formats = { 63 | BOOL: 'bool', 64 | INT: 'int', 65 | FLOAT: 'float', 66 | STRING: 'string', 67 | ARRAY: 'array', // unconfirmed 68 | DICTIONARY: 'dictionary', // unconfirmed 69 | UINT8: 'uint8', 70 | UINT16: 'uint16', 71 | UINT32: 'uint32', 72 | UINT64: 'uint64', 73 | DATA: 'data', // unconfirmed 74 | TLV8: 'tlv8' 75 | } 76 | 77 | // Known HomeKit unit types 78 | Characteristic.Units = { 79 | // HomeKit only defines Celsius, for Fahrenheit, it requires iOS app to do the conversion. 80 | CELSIUS: 'celsius', 81 | PERCENTAGE: 'percentage', 82 | ARC_DEGREE: 'arcdegrees', 83 | LUX: 'lux', 84 | SECONDS: 'seconds' 85 | } 86 | 87 | // Known HomeKit permission types 88 | Characteristic.Perms = { 89 | READ: 'pr', 90 | WRITE: 'pw', 91 | NOTIFY: 'ev', 92 | HIDDEN: 'hd' 93 | } 94 | 95 | /** 96 | * Copies the given properties to our props member variable, 97 | * and returns 'this' for chaining. 98 | * 99 | * @param 'props' { 100 | * format: , 101 | * unit: , 102 | * minValue: , 103 | * maxValue: , 104 | * minStep: , 105 | * perms: array of [Characteristic.Perms] like [Characteristic.Perms.READ, Characteristic.Perms.WRITE] 106 | * } 107 | */ 108 | Characteristic.prototype.setProps = function(props) { 109 | for (var key in (props || {})) 110 | if (Object.prototype.hasOwnProperty.call(props, key)) 111 | this.props[key] = props[key]; 112 | return this; 113 | } 114 | 115 | Characteristic.prototype.getValue = function(callback, context) { 116 | 117 | if (this.listeners('get').length > 0) { 118 | 119 | // allow a listener to handle the fetching of this value, and wait for completion 120 | this.emit('get', once(function(err, newValue) { 121 | 122 | if (err) { 123 | // pass the error along to our callback 124 | if (callback) callback(err); 125 | } 126 | else { 127 | if (newValue === undefined || newValue === null) 128 | newValue = this.getDefaultValue(); 129 | 130 | // getting the value was a success; we can pass it along and also update our cached value 131 | var oldValue = this.value; 132 | this.value = newValue; 133 | if (callback) callback(null, newValue); 134 | 135 | // emit a change event if necessary 136 | if (oldValue !== newValue) 137 | this.emit('change', { oldValue:oldValue, newValue:newValue, context:context }); 138 | } 139 | 140 | }.bind(this)), context); 141 | } 142 | else { 143 | 144 | // no one is listening to the 'get' event, so just return the cached value 145 | if (callback) 146 | callback(null, this.value); 147 | } 148 | } 149 | 150 | Characteristic.prototype.setValue = function(newValue, callback, context) { 151 | 152 | if (this.listeners('set').length > 0) { 153 | 154 | // allow a listener to handle the setting of this value, and wait for completion 155 | this.emit('set', newValue, once(function(err) { 156 | 157 | if (err) { 158 | // pass the error along to our callback 159 | if (callback) callback(err); 160 | } 161 | else { 162 | if (newValue === undefined || newValue === null) 163 | newValue = this.getDefaultValue(); 164 | // setting the value was a success; so we can cache it now 165 | var oldValue = this.value; 166 | this.value = newValue; 167 | if (callback) callback(); 168 | 169 | if (oldValue !== newValue) 170 | this.emit('change', { oldValue:oldValue, newValue:newValue, context:context }); 171 | } 172 | 173 | }.bind(this)), context); 174 | 175 | } 176 | else { 177 | if (newValue === undefined || newValue === null) 178 | newValue = this.getDefaultValue(); 179 | // no one is listening to the 'set' event, so just assign the value blindly 180 | var oldValue = this.value; 181 | this.value = newValue; 182 | if (callback) callback(); 183 | 184 | if (oldValue !== newValue) 185 | this.emit('change', { oldValue:oldValue, newValue:newValue, context:context }); 186 | } 187 | 188 | return this; // for chaining 189 | } 190 | 191 | Characteristic.prototype.getDefaultValue = function() { 192 | switch (this.props.format) { 193 | case Characteristic.Formats.BOOL: return false; 194 | case Characteristic.Formats.STRING: return ""; 195 | case Characteristic.Formats.ARRAY: return []; // who knows! 196 | case Characteristic.Formats.DICTIONARY: return {}; // who knows! 197 | case Characteristic.Formats.DATA: return ""; // who knows! 198 | case Characteristic.Formats.TLV8: return ""; // who knows! 199 | default: return this.props.minValue || 0; 200 | } 201 | } 202 | 203 | Characteristic.prototype._assignID = function(identifierCache, accessoryName, serviceUUID, serviceSubtype) { 204 | 205 | // generate our IID based on our UUID 206 | this.iid = identifierCache.getIID(accessoryName, serviceUUID, serviceSubtype, this.UUID); 207 | } 208 | 209 | /** 210 | * Returns a JSON representation of this Accessory suitable for delivering to HAP clients. 211 | */ 212 | Characteristic.prototype.toHAP = function(opt) { 213 | 214 | // ensure our value fits within our constraints if present 215 | var value = this.value; 216 | if (this.props.minValue != null && value < this.props.minValue) value = this.props.minValue; 217 | if (this.props.maxValue != null && value > this.props.maxValue) value = this.props.maxValue; 218 | if (this.props.format != null) { 219 | if (this.props.format === Characteristic.Formats.INT) 220 | value = parseInt(value); 221 | else if (this.props.format === Characteristic.Formats.UINT8) 222 | value = parseInt(value); 223 | else if (this.props.format === Characteristic.Formats.UINT16) 224 | value = parseInt(value); 225 | else if (this.props.format === Characteristic.Formats.UINT32) 226 | value = parseInt(value); 227 | else if (this.props.format === Characteristic.Formats.UINT64) 228 | value = parseInt(value); 229 | else if (this.props.format === Characteristic.Formats.FLOAT) { 230 | value = parseFloat(value); 231 | if (this.props.minStep != null) { 232 | var pow = Math.pow(10, decimalPlaces(this.props.minStep)); 233 | value = Math.round(value * pow) / pow; 234 | } 235 | } 236 | } 237 | 238 | var hap = { 239 | iid: this.iid, 240 | type: this.UUID, 241 | perms: this.props.perms, 242 | format: this.props.format, 243 | value: value, 244 | description: this.displayName 245 | 246 | // These properties used to be sent but do not seem to be used: 247 | // 248 | // events: false, 249 | // bonjour: false 250 | }; 251 | 252 | // extra properties 253 | if (this.props.unit != null) hap.unit = this.props.unit; 254 | if (this.props.maxValue != null) hap.maxValue = this.props.maxValue; 255 | if (this.props.minValue != null) hap.minValue = this.props.minValue; 256 | if (this.props.minStep != null) hap.minStep = this.props.minStep; 257 | 258 | // add maxLen if string length is > 64 bytes and trim to max 256 bytes 259 | if (this.props.format === Characteristic.Formats.STRING) { 260 | var str = new Buffer(value, 'utf8'), 261 | len = str.byteLength; 262 | if (len > 256) { // 256 bytes is the max allowed length 263 | hap.value = str.toString('utf8', 0, 256); 264 | hap.maxLen = 256; 265 | } else if (len > 64) { // values below can be ommited 266 | hap.maxLen = len; 267 | } 268 | } 269 | 270 | // if we're not readable, omit the "value" property - otherwise iOS will complain about non-compliance 271 | if (this.props.perms.indexOf(Characteristic.Perms.READ) == -1) 272 | delete hap.value; 273 | 274 | // delete the "value" property anyway if we were asked to 275 | if (opt && opt.omitValues) 276 | delete hap.value; 277 | 278 | return hap; 279 | } 280 | 281 | // Mike Samuel 282 | // http://stackoverflow.com/questions/10454518/javascript-how-to-retrieve-the-number-of-decimals-of-a-string-number 283 | function decimalPlaces(num) { 284 | var match = (''+num).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/); 285 | if (!match) { return 0; } 286 | return Math.max( 287 | 0, 288 | // Number of digits right of decimal point. 289 | (match[1] ? match[1].length : 0) 290 | // Adjust for scientific notation. 291 | - (match[2] ? +match[2] : 0)); 292 | } 293 | -------------------------------------------------------------------------------- /lib/HAPServer.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('HAPServer'); 2 | var crypto = require("crypto"); 3 | var srp = require("srp"); 4 | var url = require("url"); 5 | var inherits = require('util').inherits; 6 | var ed25519 = require("ed25519"); 7 | var hkdf = require("./util/hkdf"); 8 | var tlv = require("./util/tlv"); 9 | var encryption = require("./util/encryption") 10 | var EventEmitter = require('events').EventEmitter; 11 | var EventedHTTPServer = require("./util/eventedhttp").EventedHTTPServer; 12 | var once = require('./util/once').once; 13 | 14 | 'use strict'; 15 | 16 | module.exports = { 17 | HAPServer: HAPServer 18 | } 19 | 20 | 21 | /** 22 | * The actual HAP server that iOS devices talk to. 23 | * 24 | * Notes 25 | * ----- 26 | * It turns out that the IP-based version of HomeKit's HAP protocol operates over a sort of pseudo-HTTP. 27 | * Accessories are meant to host a TCP socket server that initially behaves exactly as an HTTP/1.1 server. 28 | * So iOS devices will open up a long-lived connection to this server and begin issuing HTTP requests. 29 | * So far, this conforms with HTTP/1.1 Keepalive. However, after the "pairing" process is complete, the 30 | * connection is expected to be "upgraded" to support full-packet encryption of both HTTP headers and data. 31 | * This encryption is NOT SSL. It is a customized ChaCha20+Poly1305 encryption layer. 32 | * 33 | * Additionally, this "HTTP Server" supports sending "event" responses at any time without warning. The iOS 34 | * device simply keeps the connection open after it's finished with HTTP request/response traffic, and while 35 | * the connection is open, the server can elect to issue "EVENT/1.0 200 OK" HTTP-style responses. These are 36 | * typically sent to inform the iOS device of a characteristic change for the accessory (like "Door was Unlocked"). 37 | * 38 | * See eventedhttp.js for more detail on the implementation of this protocol. 39 | * 40 | * @event 'listening' => function() { } 41 | * Emitted when the server is fully set up and ready to receive connections. 42 | * 43 | * @event 'identify' => function(callback(err)) { } 44 | * Emitted when a client wishes for this server to identify itself before pairing. You must call the 45 | * callback to respond to the client with success. 46 | * 47 | * @event 'pair' => function(username, publicKey, callback(err)) { } 48 | * This event is emitted when a client completes the "pairing" process and exchanges encryption keys. 49 | * Note that this does not mean the "Add Accessory" process in iOS has completed. You must call the 50 | * callback to complete the process. 51 | * 52 | * @event 'verify' => function() { } 53 | * This event is emitted after a client successfully completes the "verify" process, thereby authenticating 54 | * itself to an Accessory as a known-paired client. 55 | * 56 | * @event 'unpair' => function(username, callback(err)) { } 57 | * This event is emitted when a client has requested us to "remove their pairing info", or basically to unpair. 58 | * You must call the callback to complete the process. 59 | * 60 | * @event 'accessories' => function(callback(err, accessories)) { } 61 | * This event is emitted when a client requests the complete representation of Accessory data for 62 | * this Accessory (for instance, what services, characteristics, etc. are supported) and any bridged 63 | * Accessories in the case of a Bridge Accessory. The listener must call the provided callback function 64 | * when the accessory data is ready. We will automatically JSON.stringify the data. 65 | * 66 | * @event 'get-characteristics' => function(data, events, callback(err, characteristics)) { } 67 | * This event is emitted when a client wishes to retrieve the current value of one or more characteristics. 68 | * The listener must call the provided callback function when the values are ready. iOS clients can typically 69 | * wait up to 10 seconds for this call to return. We will automatically JSON.stringify the data (which must 70 | * be an array) and wrap it in an object with a top-level "characteristics" property. 71 | * 72 | * @event 'set-characteristics' => function(data, events, callback(err)) { } 73 | * This event is emitted when a client wishes to set the current value of one or more characteristics and/or 74 | * subscribe to one or more events. The 'events' param is an initially-empty object, associated with the current 75 | * connection, on which you may store event registration keys for later processing. The listener must call 76 | * the provided callback when the request has been processed. 77 | */ 78 | 79 | 80 | function HAPServer(accessoryInfo, relayServer) { 81 | this.accessoryInfo = accessoryInfo; 82 | this.allowInsecureRequest = false; 83 | 84 | // internal server that does all the actual communication 85 | this._httpServer = new EventedHTTPServer(); 86 | this._httpServer.on('listening', this._onListening.bind(this)); 87 | this._httpServer.on('request', this._onRequest.bind(this)); 88 | this._httpServer.on('encrypt', this._onEncrypt.bind(this)); 89 | this._httpServer.on('decrypt', this._onDecrypt.bind(this)); 90 | 91 | if (relayServer) { 92 | this._relayServer = relayServer; 93 | this._relayServer.on('request', this._onRemoteRequest.bind(this)); 94 | this._relayServer.on('encrypt', this._onEncrypt.bind(this)); 95 | this._relayServer.on('decrypt', this._onDecrypt.bind(this)); 96 | } 97 | 98 | // so iOS is very reluctant to actually disconnect HAP connections (as in, sending a FIN packet). 99 | // For instance, if you turn off wifi on your phone, it will not close the connection, instead 100 | // it will leave it open and hope that it's still valid when it returns to the network. And Node, 101 | // by itself, does not ever "discover" that the connection has been closed behind it, until a 102 | // potentially very long system-level socket timeout (like, days). To work around this, we have 103 | // invented a manual "keepalive" mechanism where we send "empty" events perodicially, such that 104 | // when Node attempts to write to the socket, it discovers that it's been disconnected after 105 | // an additional one-minute timeout (this timeout appears to be hardcoded). 106 | this._keepAliveTimerID = setInterval(this._onKeepAliveTimerTick.bind(this), 1000 * 60 * 10); // send keepalive every 10 minutes 107 | } 108 | 109 | inherits(HAPServer, EventEmitter); 110 | 111 | HAPServer.handlers = { 112 | '/identify': '_handleIdentify', 113 | '/pair-setup': '_handlePair', 114 | '/pair-verify': '_handlePairVerify', 115 | '/pairings': '_handlePairings', 116 | '/accessories': '_handleAccessories', 117 | '/characteristics': '_handleCharacteristics' 118 | } 119 | 120 | // Various "type" constants for HAP's TLV encoding. 121 | HAPServer.Types = { 122 | REQUEST_TYPE: 0x00, 123 | USERNAME: 0x01, 124 | SALT: 0x02, 125 | PUBLIC_KEY: 0x03, 126 | PASSWORD_PROOF: 0x04, 127 | ENCRYPTED_DATA: 0x05, 128 | SEQUENCE_NUM: 0x06, 129 | ERROR_CODE: 0x07, 130 | PROOF: 0x0a 131 | } 132 | 133 | // Error codes and the like, guessed by packet inspection 134 | HAPServer.Codes = { 135 | INVALID_REQUEST: 0x02, 136 | INVALID_SIGNATURE: 0x04 137 | } 138 | 139 | // Status codes for underlying HAP calls 140 | HAPServer.Status = { 141 | SUCCESS: 0, 142 | INSUFFICIENT_PRIVILEGES: -70401, 143 | SERVICE_COMMUNICATION_FAILURE: -70402, 144 | RESOURCE_BUSY: -70403, 145 | READ_ONLY_CHARACTERISTIC: -70404, 146 | WRITE_ONLY_CHARACTERISTIC: -70405, 147 | NOTIFICATION_NOT_SUPPORTED: -70406, 148 | OUT_OF_RESOURCE: -70407, 149 | OPERATION_TIMED_OUT: -70408, 150 | RESOURCE_DOES_NOT_EXIST: -70409, 151 | INVALID_VALUE_IN_REQUEST: -70410 152 | } 153 | 154 | HAPServer.prototype.listen = function(port) { 155 | this._httpServer.listen(port); 156 | } 157 | 158 | HAPServer.prototype._onKeepAliveTimerTick = function() { 159 | // send out a "keepalive" event which all connections automatically sign up for once pairVerify is 160 | // completed. The event contains no actual data, so iOS devices will simply ignore it. 161 | this.notifyClients('keepalive', {characteristics: []}); 162 | } 163 | 164 | /** 165 | * Notifies connected clients who have subscribed to a particular event. 166 | * 167 | * @param event {string} - the name of the event (only clients who have subscribed to this name will be notified) 168 | * @param data {object} - the object containing the event data; will be JSON.stringify'd automatically 169 | */ 170 | HAPServer.prototype.notifyClients = function(event, data, excludeEvents) { 171 | // encode notification data as JSON, set content-type, and hand it off to the server. 172 | this._httpServer.sendEvent(event, JSON.stringify(data), "application/hap+json", excludeEvents); 173 | 174 | if (this._relayServer) { 175 | if (event !== 'keepalive') { 176 | this._relayServer.sendEvent(event, data, excludeEvents); 177 | } 178 | } 179 | } 180 | 181 | HAPServer.prototype._onListening = function() { 182 | this.emit('listening'); 183 | } 184 | 185 | // Called when an HTTP request was detected. 186 | HAPServer.prototype._onRequest = function(request, response, session, events) { 187 | 188 | debug("[%s] HAP Request: %s %s", this.accessoryInfo.username, request.method, request.url); 189 | 190 | // collect request data, if any 191 | var requestData = Buffer(""); 192 | request.on('data', function(data) { requestData = Buffer.concat([requestData, data]); }); 193 | request.on('end', function() { 194 | 195 | // parse request.url (which can contain querystring, etc.) into components, then extract just the path 196 | var pathname = url.parse(request.url).pathname; 197 | 198 | // all request data received; now process this request 199 | for (var path in HAPServer.handlers) 200 | if (new RegExp('^'+path+'/?$').test(pathname)) { // match exact string and allow trailing slash 201 | this[HAPServer.handlers[path]](request, response, session, events, requestData); 202 | return; 203 | } 204 | 205 | // nobody handled this? reply 404 206 | debug("[%s] WARNING: Handler for %s not implemented", this.accessoryInfo.username, request.url); 207 | response.writeHead(404, "Not found", {'Content-Type': 'text/html'}); 208 | response.end(); 209 | 210 | }.bind(this)); 211 | } 212 | 213 | HAPServer.prototype._onRemoteRequest = function(request, remoteSession, session, events) { 214 | debug('[%s] Remote Request: %s', this.accessoryInfo.username, request.messageType); 215 | if (request.messageType === 'pair-verify') 216 | this._handleRemotePairVerify(request, remoteSession, session); 217 | else if (request.messageType === 'discovery') 218 | this._handleRemoteAccessories(request, remoteSession, session); 219 | else if (request.messageType === 'write-characteristics') 220 | this._handleRemoteCharacteristicsWrite(request, remoteSession, session, events); 221 | else if (request.messageType === 'read-characteristics') 222 | this._handleRemoteCharacteristicsRead(request, remoteSession, session, events); 223 | else 224 | debug('[%s] Remote Request Detail: %s', this.accessoryInfo.username, require('util').inspect(request, {showHidden: false, depth: null})); 225 | } 226 | 227 | HAPServer.prototype._onEncrypt = function(data, encrypted, session) { 228 | 229 | // instance of HAPEncryption (created in handlePairVerifyStepOne) 230 | var enc = session.encryption; 231 | 232 | // if accessoryToControllerKey is not empty, then encryption is enabled for this connection. However, we'll 233 | // need to be careful to ensure that we don't encrypt the last few bytes of the response from handlePairVerifyStepTwo. 234 | // Since all communication calls are asynchronous, we could easily receive this 'encrypt' event for those bytes. 235 | // So we want to make sure that we aren't encrypting data until we have *received* some encrypted data from the 236 | // client first. 237 | if (enc && enc.accessoryToControllerKey.length > 0 && enc.controllerToAccessoryCount.value > 0) { 238 | encrypted.data = encryption.layerEncrypt(data, enc.accessoryToControllerCount, enc.accessoryToControllerKey); 239 | } 240 | } 241 | 242 | HAPServer.prototype._onDecrypt = function(data, decrypted, session) { 243 | 244 | // possibly an instance of HAPEncryption (created in handlePairVerifyStepOne) 245 | var enc = session.encryption; 246 | 247 | // if controllerToAccessoryKey is not empty, then encryption is enabled for this connection. 248 | if (enc && enc.controllerToAccessoryKey.length > 0) { 249 | decrypted.data = encryption.layerDecrypt(data, enc.controllerToAccessoryCount, enc.controllerToAccessoryKey); 250 | } 251 | } 252 | 253 | /** 254 | * Unpaired Accessory identification. 255 | */ 256 | 257 | HAPServer.prototype._handleIdentify = function(request, response, session, events, requestData) { 258 | // /identify only works if the accesory is not paired 259 | if (!this.allowInsecureRequest && this.accessoryInfo.paired()) { 260 | response.writeHead(400, {"Content-Type": "application/hap+json"}); 261 | response.end(JSON.stringify({status:HAPServer.Status.INSUFFICIENT_PRIVILEGES})); 262 | 263 | return; 264 | } 265 | 266 | this.emit('identify', once(function(err) { 267 | 268 | if (!err) { 269 | debug("[%s] Identification success", this.accessoryInfo.username); 270 | response.writeHead(204); 271 | response.end(); 272 | } 273 | else { 274 | debug("[%s] Identification error: %s", this.accessoryInfo.username, err.message); 275 | response.writeHead(500); 276 | response.end(); 277 | } 278 | 279 | }.bind(this))); 280 | } 281 | 282 | /** 283 | * iOS <-> Accessory pairing process. 284 | */ 285 | 286 | HAPServer.prototype._handlePair = function(request, response, session, events, requestData) { 287 | // Can only be directly paired with one iOS device 288 | if (!this.allowInsecureRequest && this.accessoryInfo.paired()) { 289 | response.writeHead(403); 290 | response.end(); 291 | 292 | return; 293 | } 294 | 295 | var objects = tlv.decode(requestData); 296 | var sequence = objects[HAPServer.Types.SEQUENCE_NUM][0]; // value is single byte with sequence number 297 | 298 | if (sequence == 0x01) 299 | this._handlePairStepOne(request, response, session); 300 | else if (sequence == 0x03) 301 | this._handlePairStepTwo(request, response, session, objects); 302 | else if (sequence == 0x05) 303 | this._handlePairStepThree(request, response, session, objects); 304 | } 305 | 306 | // M1 + M2 307 | HAPServer.prototype._handlePairStepOne = function(request, response, session) { 308 | debug("[%s] Pair step 1/5", this.accessoryInfo.username); 309 | 310 | var salt = crypto.randomBytes(16); 311 | var srpParams = srp.params["3072"]; 312 | 313 | srp.genKey(32, function (error, key) { 314 | 315 | // create a new SRP server 316 | var srpServer = new srp.Server(srpParams, Buffer(salt), Buffer("Pair-Setup"), Buffer(this.accessoryInfo.pincode), key); 317 | var srpB = srpServer.computeB(); 318 | 319 | // attach it to the current TCP session 320 | session.srpServer = srpServer; 321 | 322 | response.writeHead(200, {"Content-Type": "application/pairing+tlv8"}); 323 | response.end(tlv.encode( 324 | HAPServer.Types.SEQUENCE_NUM, 0x02, 325 | HAPServer.Types.SALT, salt, 326 | HAPServer.Types.PUBLIC_KEY, srpB 327 | )); 328 | 329 | }.bind(this)); 330 | } 331 | 332 | // M3 + M4 333 | HAPServer.prototype._handlePairStepTwo = function(request, response, session, objects) { 334 | debug("[%s] Pair step 2/5", this.accessoryInfo.username); 335 | 336 | var A = objects[HAPServer.Types.PUBLIC_KEY]; // "A is a public key that exists only for a single login session." 337 | var M1 = objects[HAPServer.Types.PASSWORD_PROOF]; // "M1 is the proof that you actually know your own password." 338 | 339 | // pull the SRP server we created in stepOne out of the current session 340 | var srpServer = session.srpServer; 341 | srpServer.setA(A); 342 | 343 | try { 344 | srpServer.checkM1(M1); 345 | } 346 | catch (err) { 347 | // most likely the client supplied an incorrect pincode. 348 | debug("[%s] Error while checking pincode: %s", this.accessoryInfo.username, err.message); 349 | 350 | response.writeHead(200, {"Content-Type": "application/pairing+tlv8"}); 351 | response.end(tlv.encode( 352 | HAPServer.Types.SEQUENCE_NUM, 0x04, 353 | HAPServer.Types.ERROR_CODE, HAPServer.Codes.INVALID_REQUEST 354 | )); 355 | 356 | return; 357 | } 358 | 359 | // "M2 is the proof that the server actually knows your password." 360 | var M2 = srpServer.computeM2(); 361 | 362 | response.writeHead(200, {"Content-Type": "application/pairing+tlv8"}); 363 | response.end(tlv.encode( 364 | HAPServer.Types.SEQUENCE_NUM, 0x04, 365 | HAPServer.Types.PASSWORD_PROOF, M2 366 | )); 367 | } 368 | 369 | // M5-1 370 | HAPServer.prototype._handlePairStepThree = function(request, response, session, objects) { 371 | debug("[%s] Pair step 3/5", this.accessoryInfo.username); 372 | 373 | // pull the SRP server we created in stepOne out of the current session 374 | var srpServer = session.srpServer; 375 | 376 | var encryptedData = objects[HAPServer.Types.ENCRYPTED_DATA]; 377 | 378 | var messageData = Buffer(encryptedData.length - 16); 379 | var authTagData = Buffer(16); 380 | 381 | encryptedData.copy(messageData, 0, 0, encryptedData.length - 16); 382 | encryptedData.copy(authTagData, 0, encryptedData.length - 16, encryptedData.length); 383 | 384 | var S_private = srpServer.computeK(); 385 | var encSalt = Buffer("Pair-Setup-Encrypt-Salt"); 386 | var encInfo = Buffer("Pair-Setup-Encrypt-Info"); 387 | 388 | var outputKey = hkdf.HKDF("sha512", encSalt, S_private, encInfo, 32); 389 | 390 | var plaintextBuffer = Buffer(messageData.length); 391 | encryption.verifyAndDecrypt(outputKey, Buffer("PS-Msg05"), messageData, authTagData, null, plaintextBuffer); 392 | 393 | // decode the client payload and pass it on to the next step 394 | var M5Packet = tlv.decode(plaintextBuffer); 395 | 396 | var clientUsername = M5Packet[HAPServer.Types.USERNAME]; 397 | var clientLTPK = M5Packet[HAPServer.Types.PUBLIC_KEY]; 398 | var clientProof = M5Packet[HAPServer.Types.PROOF]; 399 | var hkdfEncKey = outputKey; 400 | 401 | this._handlePairStepFour(request, response, session, clientUsername, clientLTPK, clientProof, hkdfEncKey); 402 | } 403 | 404 | // M5-2 405 | HAPServer.prototype._handlePairStepFour = function(request, response, session, clientUsername, clientLTPK, clientProof, hkdfEncKey) { 406 | debug("[%s] Pair step 4/5", this.accessoryInfo.username); 407 | 408 | var S_private = session.srpServer.computeK(); 409 | var controllerSalt = Buffer("Pair-Setup-Controller-Sign-Salt"); 410 | var controllerInfo = Buffer("Pair-Setup-Controller-Sign-Info"); 411 | var outputKey = hkdf.HKDF("sha512", controllerSalt, S_private, controllerInfo, 32); 412 | 413 | var completeData = Buffer.concat([outputKey, clientUsername, clientLTPK]); 414 | 415 | if (!ed25519.Verify(completeData, clientProof, clientLTPK)) { 416 | debug("[%s] Invalid signature", this.accessoryInfo.username); 417 | 418 | response.writeHead(200, {"Content-Type": "application/pairing+tlv8"}); 419 | response.end(tlv.encode( 420 | HAPServer.Types.SEQUENCE_NUM, 0x06, 421 | HAPServer.Types.ERROR_CODE, HAPServer.Codes.INVALID_REQUEST 422 | )); 423 | 424 | return; 425 | } 426 | 427 | this._handlePairStepFive(request, response, session, clientUsername, clientLTPK, hkdfEncKey); 428 | } 429 | 430 | // M5 - F + M6 431 | HAPServer.prototype._handlePairStepFive = function(request, response, session, clientUsername, clientLTPK, hkdfEncKey) { 432 | debug("[%s] Pair step 5/5", this.accessoryInfo.username); 433 | 434 | var S_private = session.srpServer.computeK(); 435 | var accessorySalt = Buffer("Pair-Setup-Accessory-Sign-Salt"); 436 | var accessoryInfo = Buffer("Pair-Setup-Accessory-Sign-Info"); 437 | var outputKey = hkdf.HKDF("sha512", accessorySalt, S_private, accessoryInfo, 32); 438 | 439 | var serverLTPK = this.accessoryInfo.signPk; 440 | var usernameData = Buffer(this.accessoryInfo.username); 441 | 442 | var material = Buffer.concat([outputKey, usernameData, serverLTPK]); 443 | var privateKey = Buffer(this.accessoryInfo.signSk); 444 | 445 | var serverProof = ed25519.Sign(material, privateKey); 446 | 447 | var message = tlv.encode( 448 | HAPServer.Types.USERNAME, usernameData, 449 | HAPServer.Types.PUBLIC_KEY, serverLTPK, 450 | HAPServer.Types.PROOF, serverProof 451 | ); 452 | 453 | var ciphertextBuffer = Buffer(Array(message.length)); 454 | var macBuffer = Buffer(Array(16)); 455 | encryption.encryptAndSeal(hkdfEncKey, Buffer("PS-Msg06"), message, null, ciphertextBuffer, macBuffer); 456 | 457 | // finally, notify listeners that we have been paired with a client 458 | this.emit('pair', clientUsername.toString(), clientLTPK, once(function(err) { 459 | 460 | if (err) { 461 | debug("[%s] Error adding pairing info: %s", this.accessoryInfo.username, err.message); 462 | response.writeHead(500, "Server Error"); 463 | response.end(); 464 | return; 465 | } 466 | 467 | // send final pairing response to client 468 | response.writeHead(200, {"Content-Type": "application/pairing+tlv8"}); 469 | response.end(tlv.encode( 470 | HAPServer.Types.SEQUENCE_NUM, 0x06, 471 | HAPServer.Types.ENCRYPTED_DATA, Buffer.concat([ciphertextBuffer,macBuffer]) 472 | )); 473 | })); 474 | } 475 | 476 | /** 477 | * iOS <-> Accessory pairing verification. 478 | */ 479 | 480 | HAPServer.prototype._handlePairVerify = function(request, response, session, events, requestData) { 481 | // Don't allow pair-verify without being paired first 482 | if (!this.allowInsecureRequest && !this.accessoryInfo.paired()) { 483 | response.writeHead(403); 484 | response.end(); 485 | 486 | return; 487 | } 488 | 489 | var objects = tlv.decode(requestData); 490 | var sequence = objects[HAPServer.Types.SEQUENCE_NUM][0]; // value is single byte with sequence number 491 | 492 | if (sequence == 0x01) 493 | this._handlePairVerifyStepOne(request, response, session, objects); 494 | else if (sequence == 0x03) 495 | this._handlePairVerifyStepTwo(request, response, session, events, objects); 496 | } 497 | 498 | HAPServer.prototype._handlePairVerifyStepOne = function(request, response, session, objects) { 499 | debug("[%s] Pair verify step 1/2", this.accessoryInfo.username); 500 | 501 | var clientPublicKey = objects[HAPServer.Types.PUBLIC_KEY]; // Buffer 502 | 503 | // generate new encryption keys for this session 504 | var secretKey = encryption.generateCurve25519SecretKey(); 505 | var publicKey = encryption.generateCurve25519PublicKeyFromSecretKey(secretKey); 506 | var sharedSec = encryption.generateCurve25519SharedSecKey(secretKey, clientPublicKey); 507 | 508 | var usernameData = Buffer(this.accessoryInfo.username); 509 | var material = Buffer.concat([publicKey, usernameData, clientPublicKey]); 510 | var privateKey = Buffer(this.accessoryInfo.signSk); 511 | var serverProof = ed25519.Sign(material, privateKey); 512 | 513 | var encSalt = Buffer("Pair-Verify-Encrypt-Salt"); 514 | var encInfo = Buffer("Pair-Verify-Encrypt-Info"); 515 | 516 | var outputKey = hkdf.HKDF("sha512", encSalt, sharedSec, encInfo, 32).slice(0,32); 517 | 518 | // store keys in a new instance of HAPEncryption 519 | var enc = new HAPEncryption(); 520 | enc.clientPublicKey = clientPublicKey; 521 | enc.secretKey = secretKey; 522 | enc.publicKey = publicKey; 523 | enc.sharedSec = sharedSec; 524 | enc.hkdfPairEncKey = outputKey; 525 | 526 | // store this in the current TCP session 527 | session.encryption = enc; 528 | 529 | // compose the response data in TLV format 530 | var message = tlv.encode( 531 | HAPServer.Types.USERNAME, usernameData, 532 | HAPServer.Types.PROOF, serverProof 533 | ); 534 | 535 | // encrypt the response 536 | var ciphertextBuffer = Buffer(Array(message.length)); 537 | var macBuffer = Buffer(Array(16)); 538 | encryption.encryptAndSeal(outputKey, Buffer("PV-Msg02"), message, null, ciphertextBuffer, macBuffer); 539 | 540 | response.writeHead(200, {"Content-Type": "application/pairing+tlv8"}); 541 | response.end(tlv.encode( 542 | HAPServer.Types.SEQUENCE_NUM, 0x02, 543 | HAPServer.Types.ENCRYPTED_DATA, Buffer.concat([ciphertextBuffer, macBuffer]), 544 | HAPServer.Types.PUBLIC_KEY, publicKey 545 | )); 546 | } 547 | 548 | HAPServer.prototype._handlePairVerifyStepTwo = function(request, response, session, events, objects) { 549 | debug("[%s] Pair verify step 2/2", this.accessoryInfo.username); 550 | 551 | var encryptedData = objects[HAPServer.Types.ENCRYPTED_DATA]; 552 | 553 | var messageData = Buffer(encryptedData.length - 16); 554 | var authTagData = Buffer(16); 555 | encryptedData.copy(messageData, 0, 0, encryptedData.length - 16); 556 | encryptedData.copy(authTagData, 0, encryptedData.length - 16, encryptedData.length); 557 | 558 | var plaintextBuffer = Buffer(messageData.length); 559 | 560 | // instance of HAPEncryption (created in handlePairVerifyStepOne) 561 | var enc = session.encryption; 562 | 563 | if (!encryption.verifyAndDecrypt(enc.hkdfPairEncKey, Buffer("PV-Msg03"), messageData, authTagData, null, plaintextBuffer)) { 564 | debug("[%s] M3: Invalid signature", this.accessoryInfo.username); 565 | 566 | response.writeHead(200, {"Content-Type": "application/pairing+tlv8"}); 567 | response.end(tlv.encode( 568 | HAPServer.Types.ERROR_CODE, HAPServer.Codes.INVALID_REQUEST 569 | )); 570 | 571 | return; 572 | } 573 | 574 | var decoded = tlv.decode(plaintextBuffer); 575 | var clientUsername = decoded[HAPServer.Types.USERNAME]; 576 | var proof = decoded[HAPServer.Types.PROOF]; 577 | 578 | var material = Buffer.concat([enc.clientPublicKey, clientUsername, enc.publicKey]); 579 | 580 | // since we're paired, we should have the public key stored for this client 581 | var clientPublicKey = this.accessoryInfo.getClientPublicKey(clientUsername.toString()); 582 | 583 | // if we're not actually paired, then there's nothing to verify - this client thinks it's paired with us but we 584 | // disagree. Respond with invalid request (seems to match HomeKit Accessory Simulator behavior) 585 | if (!clientPublicKey) { 586 | debug("[%s] Client %s attempting to verify, but we are not paired; rejecting client", this.accessoryInfo.username, clientUsername); 587 | 588 | response.writeHead(200, {"Content-Type": "application/pairing+tlv8"}); 589 | response.end(tlv.encode( 590 | HAPServer.Types.ERROR_CODE, HAPServer.Codes.INVALID_REQUEST 591 | )); 592 | 593 | return; 594 | } 595 | 596 | if (!ed25519.Verify(material, proof, clientPublicKey)) { 597 | debug("[%s] Client %s provided an invalid signature", this.accessoryInfo.username, clientUsername); 598 | 599 | response.writeHead(200, {"Content-Type": "application/pairing+tlv8"}); 600 | response.end(tlv.encode( 601 | HAPServer.Types.ERROR_CODE, HAPServer.Codes.INVALID_REQUEST 602 | )); 603 | 604 | return; 605 | } 606 | 607 | debug("[%s] Client %s verification complete", this.accessoryInfo.username, clientUsername); 608 | 609 | response.writeHead(200, {"Content-Type": "application/pairing+tlv8"}); 610 | response.end(tlv.encode( 611 | HAPServer.Types.SEQUENCE_NUM, 0x04 612 | )); 613 | 614 | // now that the client has been verified, we must "upgrade" our pesudo-HTTP connection to include 615 | // TCP-level encryption. We'll do this by adding some more encryption vars to the session, and using them 616 | // in future calls to onEncrypt, onDecrypt. 617 | 618 | var encSalt = new Buffer("Control-Salt"); 619 | var infoRead = new Buffer("Control-Read-Encryption-Key"); 620 | var infoWrite = new Buffer("Control-Write-Encryption-Key"); 621 | 622 | enc.accessoryToControllerKey = hkdf.HKDF("sha512", encSalt, enc.sharedSec, infoRead, 32); 623 | enc.controllerToAccessoryKey = hkdf.HKDF("sha512", encSalt, enc.sharedSec, infoWrite, 32); 624 | 625 | // Our connection is now completely setup. We now want to subscribe this connection to special 626 | // "keepalive" events for detecting when connections are closed by the client. 627 | events['keepalive'] = true; 628 | } 629 | 630 | HAPServer.prototype._handleRemotePairVerify = function(request, remoteSession, session) { 631 | var objects = tlv.decode(request.requestBody); 632 | var sequence = objects[HAPServer.Types.SEQUENCE_NUM][0]; // value is single byte with sequence number 633 | 634 | if (sequence == 0x01) 635 | this._handleRemotePairVerifyStepOne(request, remoteSession, session, objects); 636 | else if (sequence == 0x03) 637 | this._handleRemotePairVerifyStepTwo(request, remoteSession, session, objects); 638 | } 639 | 640 | HAPServer.prototype._handleRemotePairVerifyStepOne = function(request, remoteSession, session, objects) { 641 | debug("[%s] Remote Pair verify step 1/2", this.accessoryInfo.username); 642 | 643 | var clientPublicKey = objects[HAPServer.Types.PUBLIC_KEY]; // Buffer 644 | 645 | // generate new encryption keys for this session 646 | var secretKey = encryption.generateCurve25519SecretKey(); 647 | var publicKey = encryption.generateCurve25519PublicKeyFromSecretKey(secretKey); 648 | var sharedSec = encryption.generateCurve25519SharedSecKey(secretKey, clientPublicKey); 649 | 650 | var usernameData = Buffer(this.accessoryInfo.username); 651 | var material = Buffer.concat([publicKey, usernameData, clientPublicKey]); 652 | var privateKey = Buffer(this.accessoryInfo.signSk); 653 | var serverProof = ed25519.Sign(material, privateKey); 654 | 655 | var encSalt = Buffer("Pair-Verify-Encrypt-Salt"); 656 | var encInfo = Buffer("Pair-Verify-Encrypt-Info"); 657 | 658 | var outputKey = hkdf.HKDF("sha512", encSalt, sharedSec, encInfo, 32).slice(0,32); 659 | 660 | // store keys in a new instance of HAPEncryption 661 | var enc = new HAPEncryption(); 662 | enc.clientPublicKey = clientPublicKey; 663 | enc.secretKey = secretKey; 664 | enc.publicKey = publicKey; 665 | enc.sharedSec = sharedSec; 666 | enc.hkdfPairEncKey = outputKey; 667 | 668 | // store this in the current TCP session 669 | session.encryption = enc; 670 | 671 | // compose the response data in TLV format 672 | var message = tlv.encode( 673 | HAPServer.Types.USERNAME, usernameData, 674 | HAPServer.Types.PROOF, serverProof 675 | ); 676 | 677 | // encrypt the response 678 | var ciphertextBuffer = Buffer(Array(message.length)); 679 | var macBuffer = Buffer(Array(16)); 680 | encryption.encryptAndSeal(outputKey, Buffer("PV-Msg02"), message, null, ciphertextBuffer, macBuffer); 681 | 682 | var response = tlv.encode( 683 | HAPServer.Types.SEQUENCE_NUM, 0x02, 684 | HAPServer.Types.ENCRYPTED_DATA, Buffer.concat([ciphertextBuffer, macBuffer]), 685 | HAPServer.Types.PUBLIC_KEY, publicKey 686 | ); 687 | 688 | remoteSession.responseMessage(request, response); 689 | } 690 | 691 | HAPServer.prototype._handleRemotePairVerifyStepTwo = function(request, remoteSession, session, objects) { 692 | debug("[%s] Remote Pair verify step 2/2", this.accessoryInfo.username); 693 | 694 | var encryptedData = objects[HAPServer.Types.ENCRYPTED_DATA]; 695 | 696 | var messageData = Buffer(encryptedData.length - 16); 697 | var authTagData = Buffer(16); 698 | encryptedData.copy(messageData, 0, 0, encryptedData.length - 16); 699 | encryptedData.copy(authTagData, 0, encryptedData.length - 16, encryptedData.length); 700 | 701 | var plaintextBuffer = Buffer(messageData.length); 702 | 703 | // instance of HAPEncryption (created in handlePairVerifyStepOne) 704 | var enc = session.encryption; 705 | 706 | if (!encryption.verifyAndDecrypt(enc.hkdfPairEncKey, Buffer("PV-Msg03"), messageData, authTagData, null, plaintextBuffer)) { 707 | debug("[%s] M3: Invalid signature", this.accessoryInfo.username); 708 | 709 | var response = tlv.encode( 710 | HAPServer.Types.ERROR_CODE, HAPServer.Codes.INVALID_REQUEST 711 | ); 712 | 713 | remoteSession.responseMessage(request, response); 714 | return; 715 | } 716 | 717 | var decoded = tlv.decode(plaintextBuffer); 718 | var clientUsername = decoded[HAPServer.Types.USERNAME]; 719 | var proof = decoded[HAPServer.Types.PROOF]; 720 | 721 | var material = Buffer.concat([enc.clientPublicKey, clientUsername, enc.publicKey]); 722 | 723 | // since we're paired, we should have the public key stored for this client 724 | var clientPublicKey = this.accessoryInfo.getClientPublicKey(clientUsername.toString()); 725 | 726 | // if we're not actually paired, then there's nothing to verify - this client thinks it's paired with us but we 727 | // disagree. Respond with invalid request (seems to match HomeKit Accessory Simulator behavior) 728 | if (!clientPublicKey) { 729 | debug("[%s] Client %s attempting to verify, but we are not paired; rejecting client", this.accessoryInfo.username, clientUsername); 730 | 731 | var response = tlv.encode( 732 | HAPServer.Types.ERROR_CODE, HAPServer.Codes.INVALID_REQUEST 733 | ); 734 | 735 | remoteSession.responseMessage(request, response); 736 | return; 737 | } 738 | 739 | if (!ed25519.Verify(material, proof, clientPublicKey)) { 740 | debug("[%s] Client %s provided an invalid signature", this.accessoryInfo.username, clientUsername); 741 | 742 | var response = tlv.encode( 743 | HAPServer.Types.ERROR_CODE, HAPServer.Codes.INVALID_REQUEST 744 | ); 745 | 746 | remoteSession.responseMessage(request, response); 747 | return; 748 | } 749 | 750 | debug("[%s] Client %s verification complete", this.accessoryInfo.username, clientUsername); 751 | 752 | var encSalt = new Buffer("Control-Salt"); 753 | var infoRead = new Buffer("Control-Read-Encryption-Key"); 754 | var infoWrite = new Buffer("Control-Write-Encryption-Key"); 755 | 756 | enc.accessoryToControllerKey = hkdf.HKDF("sha512", encSalt, enc.sharedSec, infoRead, 32); 757 | enc.controllerToAccessoryKey = hkdf.HKDF("sha512", encSalt, enc.sharedSec, infoWrite, 32); 758 | 759 | var response = tlv.encode( 760 | HAPServer.Types.SEQUENCE_NUM, 0x04 761 | ); 762 | 763 | remoteSession.responseMessage(request, response); 764 | } 765 | 766 | /** 767 | * Pair add/remove 768 | */ 769 | 770 | HAPServer.prototype._handlePairings = function(request, response, session, events, requestData) { 771 | 772 | // Only accept /pairing request if there is a secure session 773 | if (!this.allowInsecureRequest && !session.encryption) { 774 | response.writeHead(401, {"Content-Type": "application/hap+json"}); 775 | response.end(JSON.stringify({status:HAPServer.Status.INSUFFICIENT_PRIVILEGES})); 776 | 777 | return; 778 | } 779 | 780 | var objects = tlv.decode(requestData); 781 | var requestType = objects[HAPServer.Types.REQUEST_TYPE][0]; // value is single byte with request type 782 | 783 | if (requestType == 3) { 784 | 785 | // technically we're already paired and communicating securely if the client is able to call /pairings at all! 786 | // but maybe the client wants to change their public key? so we'll emit 'pair' like we just paired again 787 | 788 | debug("[%s] Adding pairing info for client", this.accessoryInfo.username); 789 | var clientUsername = objects[HAPServer.Types.USERNAME]; 790 | var clientLTPK = objects[HAPServer.Types.PUBLIC_KEY]; 791 | 792 | this.emit('pair', clientUsername.toString(), clientLTPK, once(function(err) { 793 | 794 | if (err) { 795 | debug("[%s] Error adding pairing info: %s", this.accessoryInfo.username, err.message); 796 | response.writeHead(500, "Server Error"); 797 | response.end(); 798 | return; 799 | } 800 | 801 | response.writeHead(200, {"Content-Type": "application/pairing+tlv8"}); 802 | response.end(tlv.encode(HAPServer.Types.SEQUENCE_NUM, 0x02)); 803 | 804 | }.bind(this))); 805 | } 806 | else if (requestType == 4) { 807 | 808 | debug("[%s] Removing pairing info for client", this.accessoryInfo.username); 809 | var clientUsername = objects[HAPServer.Types.USERNAME]; 810 | 811 | this.emit('unpair', clientUsername.toString(), once(function(err) { 812 | 813 | if (err) { 814 | debug("[%s] Error removing pairing info: %s", this.accessoryInfo.username, err.message); 815 | response.writeHead(500, "Server Error"); 816 | response.end(); 817 | return; 818 | } 819 | 820 | response.writeHead(200, {"Content-Type": "application/pairing+tlv8"}); 821 | response.end(tlv.encode(HAPServer.Types.SEQUENCE_NUM, 0x02)); 822 | 823 | }.bind(this))); 824 | } 825 | } 826 | 827 | 828 | /* 829 | * Handlers for all after-pairing communication, or the bulk of HAP. 830 | */ 831 | 832 | // Called when the client wishes to fetch all data regarding our published Accessories. 833 | HAPServer.prototype._handleAccessories = function(request, response, session, events, requestData) { 834 | if (!this.allowInsecureRequest && !session.encryption) { 835 | response.writeHead(401, {"Content-Type": "application/hap+json"}); 836 | response.end(JSON.stringify({status:HAPServer.Status.INSUFFICIENT_PRIVILEGES})); 837 | 838 | return; 839 | } 840 | 841 | // call out to listeners to retrieve the latest accessories JSON 842 | this.emit('accessories', once(function(err, accessories) { 843 | 844 | if (err) { 845 | debug("[%s] Error getting accessories: %s", this.accessoryInfo.username, err.message); 846 | response.writeHead(500, "Server Error"); 847 | response.end(); 848 | return; 849 | } 850 | 851 | response.writeHead(200, {"Content-Type": "application/hap+json"}); 852 | response.end(JSON.stringify(accessories)); 853 | })); 854 | } 855 | 856 | HAPServer.prototype._handleRemoteAccessories = function(request, remoteSession, session) { 857 | var deserializedRequest = JSON.parse(request.requestBody); 858 | 859 | if (!deserializedRequest['attribute-database']) { 860 | var response = { 861 | 'configuration-number': this.accessoryInfo.configVersion 862 | } 863 | remoteSession.responseMessage(request, new Buffer(JSON.stringify(response))); 864 | } else { 865 | var self = this; 866 | // call out to listeners to retrieve the latest accessories JSON 867 | this.emit('accessories', once(function(err, accessories) { 868 | 869 | if (err) { 870 | debug("[%s] Error getting accessories: %s", this.accessoryInfo.username, err.message); 871 | return; 872 | } 873 | 874 | var response = { 875 | 'configuration-number': self.accessoryInfo.configVersion, 876 | 'attribute-database': accessories 877 | } 878 | remoteSession.responseMessage(request, new Buffer(JSON.stringify(response))); 879 | })); 880 | } 881 | } 882 | 883 | // Called when the client wishes to get or set particular characteristics 884 | HAPServer.prototype._handleCharacteristics = function(request, response, session, events, requestData) { 885 | if (!this.allowInsecureRequest && !session.encryption) { 886 | response.writeHead(401, {"Content-Type": "application/hap+json"}); 887 | response.end(JSON.stringify({status:HAPServer.Status.INSUFFICIENT_PRIVILEGES})); 888 | 889 | return; 890 | } 891 | 892 | if (request.method == "GET") { 893 | 894 | // Extract the query params from the URL which looks like: /characteristics?id=1.9,2.14,... 895 | var parseQueryString = true; 896 | var query = url.parse(request.url, parseQueryString).query; // { id: '1.9,2.14' } 897 | 898 | if(query == undefined || query.id == undefined) { 899 | response.writeHead(500); 900 | response.end(); 901 | return; 902 | } 903 | 904 | var sets = query.id.split(','); // ["1.9","2.14"] 905 | var data = []; // [{aid:1,iid:9},{aid:2,iid:14}] 906 | 907 | for (var i in sets) { 908 | var ids = sets[i].split('.'); // ["1","9"] 909 | var aid = parseInt(ids[0]); // accessory ID 910 | var iid = parseInt(ids[1]); // instance ID (for characteristic) 911 | data.push({aid:aid,iid:iid}); 912 | } 913 | 914 | this.emit('get-characteristics', data, events, function(err, characteristics) { 915 | 916 | if (!characteristics && !err) 917 | err = new Error("characteristics not supplied by the get-characteristics event callback"); 918 | 919 | if (err) { 920 | debug("[%s] Error getting characteristics: %s", this.accessoryInfo.username, err.stack); 921 | 922 | // rewrite characteristics array to include error status for each characteristic requested 923 | characteristics = []; 924 | for (var i in data) { 925 | characteristics.push({ 926 | aid: data[i].aid, 927 | iid: data[i].iid, 928 | status: HAPServer.Status.SERVICE_COMMUNICATION_FAILURE 929 | }); 930 | } 931 | } 932 | 933 | // 207 is "multi-status" since HomeKit may be requesting multiple things and any one can fail independently 934 | response.writeHead(207, {"Content-Type": "application/hap+json"}); 935 | response.end(JSON.stringify({characteristics:characteristics})); 936 | 937 | }.bind(this)); 938 | } 939 | else if (request.method == "PUT") { 940 | if (!session.encryption) { 941 | if (!request.headers || (request.headers && request.headers["authorization"] !== this.accessoryInfo.pincode)) { 942 | response.writeHead(401, {"Content-Type": "application/hap+json"}); 943 | response.end(JSON.stringify({status:HAPServer.Status.INSUFFICIENT_PRIVILEGES})); 944 | 945 | return; 946 | } 947 | } 948 | 949 | if(requestData.length == 0) { 950 | response.writeHead(400, {"Content-Type": "application/hap+json"}); 951 | response.end(JSON.stringify({status:HAPServer.Status.INVALID_VALUE_IN_REQUEST})); 952 | 953 | return; 954 | } 955 | 956 | // requestData is a JSON payload like { characteristics: [ { aid: 1, iid: 8, value: true, ev: true } ] } 957 | var data = JSON.parse(requestData.toString()).characteristics; // pull out characteristics array 958 | 959 | // call out to listeners to retrieve the latest accessories JSON 960 | this.emit('set-characteristics', data, events, once(function(err, characteristics) { 961 | 962 | if (err) { 963 | debug("[%s] Error setting characteristics: %s", this.accessoryInfo.username, err.message); 964 | 965 | // rewrite characteristics array to include error status for each characteristic requested 966 | characteristics = []; 967 | for (var i in data) { 968 | characteristics.push({ 969 | aid: data[i].aid, 970 | iid: data[i].iid, 971 | status: HAPServer.Status.SERVICE_COMMUNICATION_FAILURE 972 | }); 973 | } 974 | } 975 | 976 | // 207 is "multi-status" since HomeKit may be setting multiple things and any one can fail independently 977 | response.writeHead(207, {"Content-Type": "application/hap+json"}); 978 | response.end(JSON.stringify({characteristics:characteristics})); 979 | 980 | }.bind(this))); 981 | } 982 | } 983 | 984 | HAPServer.prototype._handleRemoteCharacteristicsWrite = function(request, remoteSession, session, events) { 985 | var data = JSON.parse(request.requestBody.toString()); 986 | 987 | // call out to listeners to retrieve the latest accessories JSON 988 | this.emit('set-characteristics', data, events, once(function(err, characteristics) { 989 | 990 | if (err) { 991 | debug("[%s] Error setting characteristics: %s", this.accessoryInfo.username, err.message); 992 | 993 | // rewrite characteristics array to include error status for each characteristic requested 994 | characteristics = []; 995 | for (var i in data) { 996 | characteristics.push({ 997 | aid: data[i].aid, 998 | iid: data[i].iid, 999 | s: HAPServer.Status.SERVICE_COMMUNICATION_FAILURE 1000 | }); 1001 | } 1002 | } 1003 | 1004 | remoteSession.responseMessage(request, new Buffer(JSON.stringify(characteristics))); 1005 | }.bind(this)), true); 1006 | } 1007 | 1008 | HAPServer.prototype._handleRemoteCharacteristicsRead = function(request, remoteSession, session, events) { 1009 | var data = JSON.parse(request.requestBody.toString()); 1010 | 1011 | this.emit('get-characteristics', data, events, function(err, characteristics) { 1012 | 1013 | if (!characteristics && !err) 1014 | err = new Error("characteristics not supplied by the get-characteristics event callback"); 1015 | 1016 | if (err) { 1017 | debug("[%s] Error getting characteristics: %s", this.accessoryInfo.username, err.stack); 1018 | 1019 | // rewrite characteristics array to include error status for each characteristic requested 1020 | characteristics = []; 1021 | for (var i in data) { 1022 | characteristics.push({ 1023 | aid: data[i].aid, 1024 | iid: data[i].iid, 1025 | s: HAPServer.Status.SERVICE_COMMUNICATION_FAILURE 1026 | }); 1027 | } 1028 | } 1029 | remoteSession.responseMessage(request, new Buffer(JSON.stringify(characteristics))); 1030 | }.bind(this), true); 1031 | } 1032 | 1033 | /** 1034 | * Simple struct to hold vars needed to support HAP encryption. 1035 | */ 1036 | 1037 | function HAPEncryption() { 1038 | // initialize member vars with null-object values 1039 | this.clientPublicKey = new Buffer(0); 1040 | this.secretKey = new Buffer(0); 1041 | this.publicKey = new Buffer(0); 1042 | this.sharedSec = new Buffer(0); 1043 | this.hkdfPairEncKey = new Buffer(0); 1044 | this.accessoryToControllerCount = { value: 0 }; 1045 | this.controllerToAccessoryCount = { value: 0 }; 1046 | this.accessoryToControllerKey = new Buffer(0); 1047 | this.controllerToAccessoryKey = new Buffer(0); 1048 | } -------------------------------------------------------------------------------- /lib/Service.js: -------------------------------------------------------------------------------- 1 | var inherits = require('util').inherits; 2 | var clone = require('./util/clone').clone; 3 | var EventEmitter = require('events').EventEmitter; 4 | var Characteristic = require('./Characteristic').Characteristic; 5 | 6 | 'use strict'; 7 | 8 | module.exports = { 9 | Service: Service 10 | } 11 | 12 | /** 13 | * Service represents a set of grouped values necessary to provide a logical function. For instance, a 14 | * "Door Lock Mechanism" service might contain two values, one for the "desired lock state" and one for the 15 | * "current lock state". A particular Service is distinguished from others by its "type", which is a UUID. 16 | * HomeKit provides a set of known Service UUIDs defined in HomeKitTypes.js along with a corresponding 17 | * concrete subclass that you can instantiate directly to setup the necessary values. These natively-supported 18 | * Services are expected to contain a particular set of Characteristics. 19 | * 20 | * Unlike Characteristics, where you cannot have two Characteristics with the same UUID in the same Service, 21 | * you can actually have multiple Services with the same UUID in a single Accessory. For instance, imagine 22 | * a Garage Door Opener with both a "security light" and a "backlight" for the display. Each light could be 23 | * a "Lightbulb" Service with the same UUID. To account for this situation, we define an extra "subtype" 24 | * property on Service, that can be a string or other string-convertible object that uniquely identifies the 25 | * Service among its peers in an Accessory. For instance, you might have `service1.subtype = 'security_light'` 26 | * for one and `service2.subtype = 'backlight'` for the other. 27 | * 28 | * You can also define custom Services by providing your own UUID for the type that you generate yourself. 29 | * Custom Services can contain an arbitrary set of Characteristics, but Siri will likely not be able to 30 | * work with these. 31 | * 32 | * @event 'characteristic-change' => function({characteristic, oldValue, newValue, context}) { } 33 | * Emitted after a change in the value of one of our Characteristics has occurred. 34 | */ 35 | 36 | function Service(displayName, UUID, subtype) { 37 | 38 | if (!UUID) throw new Error("Services must be created with a valid UUID."); 39 | 40 | this.displayName = displayName; 41 | this.UUID = UUID; 42 | this.subtype = subtype; 43 | this.iid = null; // assigned later by our containing Accessory 44 | this.characteristics = []; 45 | this.optionalCharacteristics = []; 46 | 47 | // every service has an optional Characteristic.Name property - we'll set it to our displayName 48 | // if one was given 49 | if (displayName) { 50 | // create the characteristic if necessary 51 | var nameCharacteristic = 52 | this.getCharacteristic(Characteristic.Name) || 53 | this.addCharacteristic(Characteristic.Name); 54 | 55 | nameCharacteristic.setValue(displayName); 56 | } 57 | } 58 | 59 | inherits(Service, EventEmitter); 60 | 61 | Service.prototype.addCharacteristic = function(characteristic) { 62 | // characteristic might be a constructor like `Characteristic.Brightness` instead of an instance 63 | // of Characteristic. Coerce if necessary. 64 | if (typeof characteristic === 'function') { 65 | characteristic = new (Function.prototype.bind.apply(characteristic, arguments)); 66 | } 67 | // check for UUID conflict 68 | for (var index in this.characteristics) { 69 | var existing = this.characteristics[index]; 70 | if (existing.UUID === characteristic.UUID) 71 | throw new Error("Cannot add a Characteristic with the same UUID as another Characteristic in this Service: " + existing.UUID); 72 | } 73 | 74 | // listen for changes in characteristics and bubble them up 75 | characteristic.on('change', function(change) { 76 | // make a new object with the relevant characteristic added, and bubble it up 77 | this.emit('characteristic-change', clone(change, {characteristic:characteristic})); 78 | }.bind(this)); 79 | 80 | this.characteristics.push(characteristic); 81 | 82 | this.emit('service-configurationChange', clone({service:this})); 83 | 84 | return characteristic; 85 | } 86 | 87 | Service.prototype.removeCharacteristic = function(characteristic) { 88 | var targetCharacteristicIndex; 89 | 90 | for (var index in this.characteristics) { 91 | var existingCharacteristic = this.characteristics[index]; 92 | 93 | if (existingCharacteristic === characteristic) { 94 | targetCharacteristicIndex = index; 95 | break; 96 | } 97 | } 98 | 99 | if (targetCharacteristicIndex) { 100 | this.characteristics.splice(targetCharacteristicIndex, 1); 101 | characteristic.removeAllListeners(); 102 | 103 | this.emit('service-configurationChange', clone({service:this})); 104 | } 105 | } 106 | 107 | Service.prototype.getCharacteristic = function(name) { 108 | // returns a characteristic object from the service 109 | // If Service.prototype.getCharacteristic(Characteristic.Type) does not find the characteristic, 110 | // but the type is in optionalCharacteristics, it adds the characteristic.type to the service and returns it. 111 | var index, characteristic; 112 | for (index in this.characteristics) { 113 | characteristic = this.characteristics[index]; 114 | if (typeof name === 'string' && characteristic.displayName === name) { 115 | return characteristic; 116 | } 117 | else if (typeof name === 'function' && ((characteristic instanceof name) || (name.UUID === characteristic.UUID))) { 118 | return characteristic; 119 | } 120 | } 121 | if (typeof name === 'function') { 122 | for (index in this.optionalCharacteristics) { 123 | characteristic = this.optionalCharacteristics[index]; 124 | if ((characteristic instanceof name) || (name.UUID === characteristic.UUID)) { 125 | return this.addCharacteristic(name); 126 | } 127 | } 128 | } 129 | }; 130 | 131 | Service.prototype.testCharacteristic = function(name) { 132 | // checks for the existence of a characteristic object in the service 133 | var index, characteristic; 134 | for (index in this.characteristics) { 135 | characteristic = this.characteristics[index]; 136 | if (typeof name === 'string' && characteristic.displayName === name) { 137 | return true; 138 | } 139 | else if (typeof name === 'function' && ((characteristic instanceof name) || (name.UUID === characteristic.UUID))) { 140 | return true; 141 | } 142 | } 143 | return false; 144 | } 145 | 146 | Service.prototype.setCharacteristic = function(name, value) { 147 | this.getCharacteristic(name).setValue(value); 148 | return this; // for chaining 149 | } 150 | 151 | Service.prototype.addOptionalCharacteristic = function(characteristic) { 152 | // characteristic might be a constructor like `Characteristic.Brightness` instead of an instance 153 | // of Characteristic. Coerce if necessary. 154 | if (typeof characteristic === 'function') 155 | characteristic = new characteristic(); 156 | 157 | this.optionalCharacteristics.push(characteristic); 158 | } 159 | 160 | Service.prototype.getCharacteristicByIID = function(iid) { 161 | for (var index in this.characteristics) { 162 | var characteristic = this.characteristics[index]; 163 | if (characteristic.iid === iid) 164 | return characteristic; 165 | } 166 | } 167 | 168 | Service.prototype._assignIDs = function(identifierCache, accessoryName) { 169 | 170 | // the Accessory Information service must have a (reserved by IdentifierCache) ID of 1 171 | if (this.UUID === '0000003E-0000-1000-8000-0026BB765291') { 172 | this.iid = 1; 173 | } 174 | else { 175 | // assign our own ID based on our UUID 176 | this.iid = identifierCache.getIID(accessoryName, this.UUID, this.subtype); 177 | } 178 | 179 | // assign IIDs to our Characteristics 180 | for (var index in this.characteristics) { 181 | var characteristic = this.characteristics[index]; 182 | characteristic._assignID(identifierCache, accessoryName, this.UUID, this.subtype); 183 | } 184 | } 185 | 186 | /** 187 | * Returns a JSON representation of this Accessory suitable for delivering to HAP clients. 188 | */ 189 | Service.prototype.toHAP = function(opt) { 190 | 191 | var characteristicsHAP = []; 192 | 193 | for (var index in this.characteristics) { 194 | var characteristic = this.characteristics[index]; 195 | characteristicsHAP.push(characteristic.toHAP(opt)); 196 | } 197 | 198 | return { 199 | iid: this.iid, 200 | type: this.UUID, 201 | characteristics: characteristicsHAP 202 | } 203 | } 204 | 205 | Service.prototype._setupCharacteristic = function(characteristic) { 206 | // listen for changes in characteristics and bubble them up 207 | characteristic.on('change', function(change) { 208 | // make a new object with the relevant characteristic added, and bubble it up 209 | this.emit('characteristic-change', clone(change, {characteristic:characteristic})); 210 | }.bind(this)); 211 | } 212 | 213 | Service.prototype._sideloadCharacteristics = function(targetCharacteristics) { 214 | for (var index in targetCharacteristics) { 215 | var target = targetCharacteristics[index]; 216 | this._setupCharacteristic(target); 217 | } 218 | 219 | this.characteristics = targetCharacteristics; 220 | } -------------------------------------------------------------------------------- /lib/gen/import.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | var plist = require('simple-plist'); 4 | var Characteristic = require('../Characteristic').Characteristic; 5 | 6 | /** 7 | * This module is intended to be run from the command line. It is a script that extracts Apple's Service 8 | * and Characteristic UUIDs and structures from Apple's own HomeKit Accessory Simulator app. 9 | */ 10 | 11 | // assumed location of the plist we need (might want to make this a command-line argument at some point) 12 | var plistPath = '/Applications/HomeKit Accessory Simulator.app/Contents/Frameworks/HAPAccessoryKit.framework/Versions/A/Resources/default.metadata.plist'; 13 | var metadata = plist.readFileSync(plistPath); 14 | 15 | // begin writing the output file 16 | var outputPath = path.join(__dirname, 'HomeKitTypes.js'); 17 | var output = fs.createWriteStream(outputPath); 18 | 19 | output.write("// THIS FILE IS AUTO-GENERATED - DO NOT MODIFY\n"); 20 | output.write("\n"); 21 | output.write("var inherits = require('util').inherits;\n"); 22 | output.write("var Characteristic = require('../Characteristic').Characteristic;\n"); 23 | output.write("var Service = require('../Service').Service;\n"); 24 | output.write("\n"); 25 | 26 | /** 27 | * Characteristics 28 | */ 29 | 30 | // index Characteristics for quick access while building Services 31 | var characteristics = {}; // characteristics[UUID] = classyName 32 | 33 | for (var index in metadata.Characteristics) { 34 | var characteristic = metadata.Characteristics[index]; 35 | var classyName = characteristic.Name.replace(/[\s\-]/g, ""); // "Target Door State" -> "TargetDoorState" 36 | 37 | // index classyName for when we want to declare these in Services below 38 | characteristics[characteristic.UUID] = classyName; 39 | 40 | output.write("/**\n * Characteristic \"" + characteristic.Name + "\"\n */\n\n"); 41 | output.write("Characteristic." + classyName + " = function() {\n"); 42 | output.write(" Characteristic.call(this, '" + characteristic.Name + "', '" + characteristic.UUID + "');\n"); 43 | 44 | // apply Characteristic properties 45 | output.write(" this.setProps({\n"); 46 | output.write(" format: Characteristic.Formats." + getCharacteristicFormatsKey(characteristic.Format)); 47 | 48 | // special unit type? 49 | if (characteristic.Unit) 50 | output.write(",\n unit: Characteristic.Units." + getCharacteristicUnitsKey(characteristic.Unit)); 51 | 52 | // apply any basic constraints if present 53 | if (characteristic.Constraints && typeof characteristic.Constraints.MaximumValue !== 'undefined') 54 | output.write(",\n maxValue: " + characteristic.Constraints.MaximumValue); 55 | 56 | if (characteristic.Constraints && typeof characteristic.Constraints.MinimumValue !== 'undefined') 57 | output.write(",\n minValue: " + characteristic.Constraints.MinimumValue); 58 | 59 | if (characteristic.Constraints && typeof characteristic.Constraints.StepValue !== 'undefined') 60 | output.write(",\n minStep: " + characteristic.Constraints.StepValue); 61 | 62 | output.write(",\n perms: ["); 63 | var sep = "" 64 | for (var i in characteristic.Properties) { 65 | var perms = getCharacteristicPermsKey(characteristic.Properties[i]); 66 | if (perms) { 67 | output.write(sep + "Characteristic.Perms." + getCharacteristicPermsKey(characteristic.Properties[i])); 68 | sep = ", " 69 | } 70 | } 71 | output.write("]"); 72 | 73 | output.write("\n });\n"); 74 | 75 | // set default value 76 | output.write(" this.value = this.getDefaultValue();\n"); 77 | 78 | output.write("};\n\n"); 79 | output.write("inherits(Characteristic." + classyName + ", Characteristic);\n\n"); 80 | output.write("Characteristic." + classyName + ".UUID = '" + characteristic.UUID + "';\n\n"); 81 | 82 | if (characteristic.Constraints && characteristic.Constraints.ValidValues) { 83 | // this characteristic can only have one of a defined set of values (like an enum). Define the values 84 | // as static members of our subclass. 85 | output.write("// The value property of " + classyName + " must be one of the following:\n"); 86 | 87 | for (var value in characteristic.Constraints.ValidValues) { 88 | var name = characteristic.Constraints.ValidValues[value]; 89 | 90 | var constName = name.toUpperCase().replace(/[^\w]+/g, '_'); 91 | if ((/^[1-9]/).test(constName)) constName = "_" + constName; // variables can't start with a number 92 | output.write("Characteristic." + classyName + "." + constName + " = " + value + ";\n"); 93 | } 94 | 95 | output.write("\n"); 96 | } 97 | } 98 | 99 | 100 | /** 101 | * Services 102 | */ 103 | 104 | for (var index in metadata.Services) { 105 | var service = metadata.Services[index]; 106 | var classyName = service.Name.replace(/[\s\-]/g, ""); // "Smoke Sensor" -> "SmokeSensor" 107 | 108 | output.write("/**\n * Service \"" + service.Name + "\"\n */\n\n"); 109 | output.write("Service." + classyName + " = function(displayName, subtype) {\n"); 110 | // call superclass constructor 111 | output.write(" Service.call(this, displayName, '" + service.UUID + "', subtype);\n"); 112 | 113 | // add Characteristics for this Service 114 | if (service.RequiredCharacteristics) { 115 | output.write("\n // Required Characteristics\n"); 116 | 117 | for (var index in service.RequiredCharacteristics) { 118 | var characteristicUUID = service.RequiredCharacteristics[index]; 119 | 120 | // look up the classyName from the hash we built above 121 | var characteristicClassyName = characteristics[characteristicUUID]; 122 | 123 | output.write(" this.addCharacteristic(Characteristic." + characteristicClassyName + ");\n"); 124 | } 125 | } 126 | 127 | // add "Optional" Characteristics for this Service 128 | if (service.OptionalCharacteristics) { 129 | output.write("\n // Optional Characteristics\n"); 130 | 131 | for (var index in service.OptionalCharacteristics) { 132 | var characteristicUUID = service.OptionalCharacteristics[index]; 133 | 134 | // look up the classyName from the hash we built above 135 | var characteristicClassyName = characteristics[characteristicUUID]; 136 | 137 | output.write(" this.addOptionalCharacteristic(Characteristic." + characteristicClassyName + ");\n"); 138 | } 139 | } 140 | 141 | output.write("};\n\n"); 142 | output.write("inherits(Service." + classyName + ", Service);\n\n"); 143 | output.write("Service." + classyName + ".UUID = '" + service.UUID + "';\n\n"); 144 | } 145 | 146 | 147 | /** 148 | * Done! 149 | */ 150 | 151 | output.end(); 152 | 153 | /** 154 | * Useful functions 155 | */ 156 | 157 | function getCharacteristicFormatsKey(format) { 158 | // coerce 'int32' to 'int' 159 | if (format == 'int32') format = 'int'; 160 | 161 | // look up the key in our known-formats dict 162 | for (var key in Characteristic.Formats) 163 | if (Characteristic.Formats[key] == format) 164 | return key; 165 | 166 | throw new Error("Unknown characteristic format '" + format + "'"); 167 | } 168 | 169 | function getCharacteristicUnitsKey(units) { 170 | // look up the key in our known-units dict 171 | for (var key in Characteristic.Units) 172 | if (Characteristic.Units[key] == units) 173 | return key; 174 | 175 | throw new Error("Unknown characteristic units '" + units + "'"); 176 | } 177 | 178 | function getCharacteristicPermsKey(perm) { 179 | switch (perm) { 180 | case "read": return "READ"; 181 | case "write": return "WRITE"; 182 | case "cnotify": return "NOTIFY"; 183 | case "uncnotify": return undefined; 184 | default: throw new Error("Unknown characteristic permission '" + perm + "'"); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /lib/model/AccessoryInfo.js: -------------------------------------------------------------------------------- 1 | var storage = require('node-persist'); 2 | var util = require('util'); 3 | var crypto = require('crypto'); 4 | var ed25519 = require('ed25519'); 5 | 6 | 'use strict'; 7 | 8 | module.exports = { 9 | AccessoryInfo: AccessoryInfo 10 | }; 11 | 12 | 13 | /** 14 | * AccessoryInfo is a model class containing a subset of Accessory data relevant to the internal HAP server, 15 | * such as encryption keys and username. It is persisted to disk. 16 | */ 17 | 18 | function AccessoryInfo(username) { 19 | this.username = username; 20 | this.port = 0; 21 | this.displayName = ""; 22 | this.category = ""; 23 | this.pincode = ""; 24 | this.signSk = Buffer(0); 25 | this.signPk = Buffer(0); 26 | this.pairedClients = {}; // pairedClients[clientUsername:string] = clientPublicKey:Buffer 27 | this.configVersion = 1; 28 | this.configHash = ""; 29 | 30 | this.relayEnabled = false; 31 | this.relayState = 2; 32 | this.relayAccessoryID = ""; 33 | this.relayAdminID = ""; 34 | this.relayPairedControllers = {}; 35 | this.accessoryBagURL = ""; 36 | } 37 | 38 | // Add a paired client to our memory. 'publicKey' should be an instance of Buffer. 39 | AccessoryInfo.prototype.addPairedClient = function(username, publicKey) { 40 | this.pairedClients[username] = publicKey; 41 | } 42 | 43 | // Add a paired client to our memory. 44 | AccessoryInfo.prototype.removePairedClient = function(username) { 45 | delete this.pairedClients[username]; 46 | 47 | if (Object.keys(this.pairedClients).length == 0) { 48 | this.relayEnabled = false; 49 | this.relayState = 2; 50 | this.relayAccessoryID = ""; 51 | this.relayAdminID = ""; 52 | this.relayPairedControllers = {}; 53 | this.accessoryBagURL = ""; 54 | } 55 | } 56 | 57 | // Gets the public key for a paired client as a Buffer, or falsey value if not paired. 58 | AccessoryInfo.prototype.getClientPublicKey = function(username) { 59 | return this.pairedClients[username]; 60 | } 61 | 62 | // Returns a boolean indicating whether this accessory has been paired with a client. 63 | AccessoryInfo.prototype.paired = function() { 64 | return Object.keys(this.pairedClients).length > 0; // if we have any paired clients, we're paired. 65 | } 66 | 67 | AccessoryInfo.prototype.updateRelayEnableState = function(state) { 68 | this.relayEnabled = state; 69 | } 70 | 71 | AccessoryInfo.prototype.updateRelayState = function(newState) { 72 | this.relayState = newState; 73 | } 74 | 75 | AccessoryInfo.prototype.addPairedRelayClient = function(username, accessToken) { 76 | this.relayPairedControllers[username] = accessToken; 77 | } 78 | 79 | AccessoryInfo.prototype.removePairedRelayClient = function(username) { 80 | delete this.relayPairedControllers[username]; 81 | } 82 | 83 | // Gets a key for storing this AccessoryInfo in the filesystem, like "AccessoryInfo.CC223DE3CEF3.json" 84 | AccessoryInfo.persistKey = function(username) { 85 | return util.format("AccessoryInfo.%s.json", username.replace(/:/g,"").toUpperCase()); 86 | } 87 | 88 | AccessoryInfo.create = function(username) { 89 | var accessoryInfo = new AccessoryInfo(username); 90 | 91 | // Create a new unique key pair for this accessory. 92 | var seed = crypto.randomBytes(32); 93 | var keyPair = ed25519.MakeKeypair(seed); 94 | 95 | accessoryInfo.signSk = keyPair.privateKey; 96 | accessoryInfo.signPk = keyPair.publicKey; 97 | 98 | return accessoryInfo; 99 | } 100 | 101 | AccessoryInfo.load = function(username) { 102 | var key = AccessoryInfo.persistKey(username); 103 | var saved = storage.getItem(key); 104 | 105 | if (saved) { 106 | var info = new AccessoryInfo(username); 107 | info.port = saved.port; 108 | info.displayName = saved.displayName || ""; 109 | info.category = saved.category || ""; 110 | info.pincode = saved.pincode || ""; 111 | info.signSk = new Buffer(saved.signSk || '', 'hex'); 112 | info.signPk = new Buffer(saved.signPk || '', 'hex'); 113 | 114 | info.pairedClients = {}; 115 | for (var username in saved.pairedClients || {}) { 116 | var publicKey = saved.pairedClients[username]; 117 | info.pairedClients[username] = new Buffer(publicKey, 'hex'); 118 | } 119 | 120 | info.configVersion = saved.configVersion || 1; 121 | info.configHash = saved.configHash || ""; 122 | 123 | info.relayEnabled = saved.relayEnabled || false; 124 | info.relayState = saved.relayState || 2; 125 | info.relayAccessoryID = saved.relayAccessoryID || ""; 126 | info.relayAdminID = saved.relayAdminID || ""; 127 | info.relayPairedControllers = saved.relayPairedControllers || {}; 128 | info.accessoryBagURL = saved.accessoryBagURL || ""; 129 | 130 | return info; 131 | } 132 | else { 133 | return null; 134 | } 135 | } 136 | 137 | AccessoryInfo.prototype.save = function() { 138 | var saved = { 139 | displayName: this.displayName, 140 | port: this.port, 141 | category: this.category, 142 | pincode: this.pincode, 143 | signSk: this.signSk.toString('hex'), 144 | signPk: this.signPk.toString('hex'), 145 | pairedClients: {}, 146 | configVersion: this.configVersion, 147 | configHash: this.configHash, 148 | relayEnabled: this.relayEnabled, 149 | relayState: this.relayState, 150 | relayAccessoryID: this.relayAccessoryID, 151 | relayAdminID: this.relayAdminID, 152 | relayPairedControllers: this.relayPairedControllers, 153 | accessoryBagURL: this.accessoryBagURL 154 | }; 155 | 156 | for (var username in this.pairedClients) { 157 | var publicKey = this.pairedClients[username]; 158 | saved.pairedClients[username] = publicKey.toString('hex'); 159 | } 160 | 161 | var key = AccessoryInfo.persistKey(this.username); 162 | 163 | storage.setItemSync(key, saved); 164 | storage.persistSync(); 165 | } 166 | -------------------------------------------------------------------------------- /lib/model/IdentifierCache.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var storage = require('node-persist'); 3 | 4 | 'use strict'; 5 | 6 | module.exports = { 7 | IdentifierCache: IdentifierCache 8 | }; 9 | 10 | 11 | /** 12 | * IdentifierCache is a model class that manages a system of associating HAP "Accessory IDs" and "Instance IDs" 13 | * with other values that don't usually change. HomeKit Clients use Accessory/Instance IDs as a primary key of 14 | * sorts, so the IDs need to remain "stable". For instance, if you create a HomeKit "Scene" called "Leaving Home" 15 | * that sets your Alarm System's "Target Alarm State" Characteristic to "Arm Away", that Scene will store whatever 16 | * "Instance ID" it was given for the "Target Alarm State" Characteristic. If the ID changes later on this server, 17 | * the scene will stop working. 18 | */ 19 | 20 | function IdentifierCache(username) { 21 | this.username = username; 22 | this._cache = {}; // cache[key:string] = id:number 23 | this._usedCache = null; // for usage tracking and expiring old keys 24 | } 25 | 26 | IdentifierCache.prototype.startTrackingUsage = function() { 27 | this._usedCache = {}; 28 | } 29 | 30 | IdentifierCache.prototype.stopTrackingUsageAndExpireUnused = function() { 31 | // simply rotate in the new cache that was built during our normal getXYZ() calls. 32 | this._cache = this._usedCache; 33 | this._usedCache = null; 34 | } 35 | 36 | IdentifierCache.prototype.getCache = function(key) { 37 | var value = this._cache[key]; 38 | 39 | // track this cache item if needed 40 | if (this._usedCache && typeof value !== 'undefined') 41 | this._usedCache[key] = value; 42 | 43 | return value; 44 | } 45 | 46 | IdentifierCache.prototype.setCache = function(key, value) { 47 | this._cache[key] = value; 48 | 49 | // track this cache item if needed 50 | if (this._usedCache) 51 | this._usedCache[key] = value; 52 | 53 | return value; 54 | } 55 | 56 | IdentifierCache.prototype.getAID = function(accessoryUUID) { 57 | var key = accessoryUUID; 58 | 59 | // ensure that our "next AID" field is not expired 60 | this.getCache('|nextAID'); 61 | 62 | return this.getCache(key) || this.setCache(key, this.getNextAID()); 63 | } 64 | 65 | IdentifierCache.prototype.getIID = function(accessoryUUID, serviceUUID, serviceSubtype, characteristicUUID) { 66 | 67 | var key = accessoryUUID 68 | + '|' + serviceUUID 69 | + (serviceSubtype ? '|' + serviceSubtype : '') 70 | + (characteristicUUID ? '|' + characteristicUUID : ''); 71 | 72 | // ensure that our "next IID" field for this accessory is not expired 73 | this.getCache(accessoryUUID + '|nextIID'); 74 | 75 | return this.getCache(key) || this.setCache(key, this.getNextIID(accessoryUUID)); 76 | } 77 | 78 | IdentifierCache.prototype.getNextAID = function() { 79 | var key = '|nextAID'; 80 | var nextAID = this.getCache(key) || 2; // start at 2 because the root Accessory or Bridge must be 1 81 | this.setCache(key, nextAID + 1); // increment 82 | return nextAID; 83 | } 84 | 85 | IdentifierCache.prototype.getNextIID = function(accessoryUUID) { 86 | var key = accessoryUUID + '|nextIID'; 87 | var nextIID = this.getCache(key) || 2; // iid 1 is reserved for the Accessory Information service 88 | this.setCache(key, nextIID + 1); // increment 89 | return nextIID; 90 | } 91 | 92 | /** 93 | * Persisting to File System 94 | */ 95 | 96 | // Gets a key for storing this IdentifierCache in the filesystem, like "IdentifierCache.CC223DE3CEF3.json" 97 | IdentifierCache.persistKey = function(username) { 98 | return util.format("IdentifierCache.%s.json", username.replace(/:/g,"").toUpperCase()); 99 | } 100 | 101 | IdentifierCache.load = function(username) { 102 | var key = IdentifierCache.persistKey(username); 103 | var saved = storage.getItem(key); 104 | 105 | if (saved) { 106 | var info = new IdentifierCache(username); 107 | info._cache = saved.cache; 108 | return info; 109 | } 110 | else { 111 | return null; 112 | } 113 | } 114 | 115 | IdentifierCache.prototype.save = function() { 116 | var saved = { 117 | cache: this._cache 118 | }; 119 | 120 | var key = IdentifierCache.persistKey(this.username); 121 | 122 | storage.setItemSync(key, saved); 123 | storage.persistSync(); 124 | } 125 | -------------------------------------------------------------------------------- /lib/util/chacha20poly1305.js: -------------------------------------------------------------------------------- 1 | /* chacha20 - 256 bits */ 2 | 3 | // Written in 2014 by Devi Mandiri. Public domain. 4 | // 5 | // Implementation derived from chacha-ref.c version 20080118 6 | // See for details: http://cr.yp.to/chacha/chacha-20080128.pdf 7 | 8 | module.exports = { 9 | Chacha20Ctx: Chacha20Ctx, 10 | chacha20_keysetup: chacha20_keysetup, 11 | chacha20_ivsetup: chacha20_ivsetup, 12 | chacha20_keystream: chacha20_keystream, 13 | chacha20_update: chacha20_update, 14 | chacha20_final: chacha20_final, 15 | 16 | Poly1305Ctx: Poly1305Ctx, 17 | poly1305_init: poly1305_init, 18 | poly1305_update: poly1305_update, 19 | poly1305_finish: poly1305_finish, 20 | poly1305_auth: poly1305_auth, 21 | poly1305_verify: poly1305_verify 22 | }; 23 | 24 | var Chacha20KeySize = 32; 25 | var Chacha20NonceSize = 8; 26 | 27 | function Chacha20Ctx() { 28 | this.input = new Array(16); 29 | this.leftover = 0; 30 | this.buffer = new Buffer(Array(64)); 31 | }; 32 | 33 | function load32(x, i) { 34 | return x[i] | (x[i+1]<<8) | (x[i+2]<<16) | (x[i+3]<<24); 35 | } 36 | 37 | function store32(x, i, u) { 38 | x[i] = u & 0xff; u >>>= 8; 39 | x[i+1] = u & 0xff; u >>>= 8; 40 | x[i+2] = u & 0xff; u >>>= 8; 41 | x[i+3] = u & 0xff; 42 | } 43 | 44 | function plus(v, w) { 45 | return (v + w) >>> 0; 46 | } 47 | 48 | function rotl32(v, c) { 49 | return ((v << c) >>> 0) | (v >>> (32 - c)); 50 | } 51 | 52 | function quarterRound(x, a, b, c, d) { 53 | x[a] = plus(x[a], x[b]); x[d] = rotl32(x[d] ^ x[a], 16); 54 | x[c] = plus(x[c], x[d]); x[b] = rotl32(x[b] ^ x[c], 12); 55 | x[a] = plus(x[a], x[b]); x[d] = rotl32(x[d] ^ x[a], 8); 56 | x[c] = plus(x[c], x[d]); x[b] = rotl32(x[b] ^ x[c], 7); 57 | } 58 | 59 | function chacha20_keysetup(ctx, key) { 60 | ctx.input[0] = 1634760805; 61 | ctx.input[1] = 857760878; 62 | ctx.input[2] = 2036477234; 63 | ctx.input[3] = 1797285236; 64 | for (var i = 0; i < 8; i++) { 65 | ctx.input[i+4] = load32(key, i*4); 66 | } 67 | } 68 | 69 | function chacha20_ivsetup(ctx, iv) { 70 | ctx.input[12] = 0; 71 | ctx.input[13] = 0; 72 | ctx.input[14] = load32(iv, 0); 73 | ctx.input[15] = load32(iv, 4); 74 | } 75 | 76 | function chacha20_encrypt(ctx, dst, src, len) { 77 | var x = new Array(16); 78 | var buf = new Array(64); 79 | var i = 0, dpos = 0, spos = 0; 80 | 81 | while (len > 0 ) { 82 | for (i = 16; i--;) x[i] = ctx.input[i]; 83 | for (i = 20; i > 0; i -= 2) { 84 | quarterRound(x, 0, 4, 8,12); 85 | quarterRound(x, 1, 5, 9,13); 86 | quarterRound(x, 2, 6,10,14); 87 | quarterRound(x, 3, 7,11,15); 88 | quarterRound(x, 0, 5,10,15); 89 | quarterRound(x, 1, 6,11,12); 90 | quarterRound(x, 2, 7, 8,13); 91 | quarterRound(x, 3, 4, 9,14); 92 | } 93 | for (i = 16; i--;) x[i] += ctx.input[i]; 94 | for (i = 16; i--;) store32(buf, 4*i, x[i]); 95 | 96 | ctx.input[12] = plus(ctx.input[12], 1); 97 | if (!ctx.input[12]) { 98 | ctx.input[13] = plus(ctx.input[13], 1); 99 | } 100 | if (len <= 64) { 101 | for (i = len; i--;) { 102 | dst[i+dpos] = src[i+spos] ^ buf[i]; 103 | } 104 | return; 105 | } 106 | for (i = 64; i--;) { 107 | dst[i+dpos] = src[i+spos] ^ buf[i]; 108 | } 109 | len -= 64; 110 | spos += 64; 111 | dpos += 64; 112 | } 113 | } 114 | 115 | function chacha20_decrypt(ctx, dst, src, len) { 116 | chacha20_encrypt(ctx, dst, src, len); 117 | } 118 | 119 | function chacha20_update(ctx, dst, src, inlen) { 120 | var bytes = 0; 121 | var out_start = 0; 122 | var out_inc = 0; 123 | 124 | if ((ctx.leftover + inlen) >= 64) { 125 | 126 | if (ctx.leftover != 0) { 127 | bytes = 64 - ctx.leftover; 128 | 129 | if (src.length > 0) { 130 | src.copy(ctx.buffer,ctx.leftover,0,bytes); 131 | src = src.slice(bytes,src.length); 132 | } 133 | 134 | chacha20_encrypt(ctx,dst,ctx.buffer,64); 135 | inlen -= bytes; 136 | dst = dst.slice(64,dst.length); 137 | out_inc += 64; 138 | ctx.leftover = 0; 139 | } 140 | 141 | bytes = (inlen & (~63)); 142 | if (bytes > 0) { 143 | chacha20_encrypt(ctx, dst, src, bytes); 144 | inlen -= bytes; 145 | src = src.slice(bytes,src.length); 146 | dst = dst.slice(bytes,dst.length); 147 | out_inc += bytes; 148 | } 149 | 150 | } 151 | 152 | if (inlen > 0) { 153 | if (src.length > 0) { 154 | src.copy(ctx.buffer,ctx.leftover,0,src.length); 155 | } else { 156 | var zeros = Buffer(Array(inlen)); 157 | zeros.copy(ctx.buffer,ctx.leftover,0,inlen); 158 | } 159 | ctx.leftover += inlen; 160 | } 161 | 162 | return out_inc - out_start; 163 | } 164 | 165 | function chacha20_final(ctx, dst) { 166 | if (ctx.leftover != 0) { 167 | chacha20_encrypt(ctx, dst, ctx.buffer, 64); 168 | } 169 | 170 | return ctx.leftover; 171 | } 172 | 173 | function chacha20_keystream(ctx, dst, len) { 174 | for (var i = 0; i < len; ++i) dst[i] = 0; 175 | chacha20_encrypt(ctx, dst, dst, len); 176 | } 177 | 178 | /* poly1305 */ 179 | 180 | // Written in 2014 by Devi Mandiri. Public domain. 181 | // 182 | // Implementation derived from poly1305-donna-16.h 183 | // See for details: https://github.com/floodyberry/poly1305-donna 184 | 185 | var Poly1305KeySize = 32; 186 | var Poly1305TagSize = 16; 187 | 188 | function Poly1305Ctx() { 189 | this.buffer = new Array(Poly1305TagSize); 190 | this.leftover = 0; 191 | this.r = new Array(10); 192 | this.h = new Array(10); 193 | this.pad = new Array(8); 194 | this.finished = 0; 195 | }; 196 | 197 | function U8TO16(p, pos) { 198 | return ((p[pos] & 0xff) & 0xffff) | (((p[pos+1] & 0xff) & 0xffff) << 8); 199 | } 200 | 201 | function U16TO8(p, pos, v) { 202 | p[pos] = (v ) & 0xff; 203 | p[pos+1] = (v >>> 8) & 0xff; 204 | } 205 | 206 | function poly1305_init(ctx, key) { 207 | var t = [], i = 0; 208 | 209 | for (i = 8; i--;) t[i] = U8TO16(key, i*2); 210 | 211 | ctx.r[0] = t[0] & 0x1fff; 212 | ctx.r[1] = ((t[0] >>> 13) | (t[1] << 3)) & 0x1fff; 213 | ctx.r[2] = ((t[1] >>> 10) | (t[2] << 6)) & 0x1f03; 214 | ctx.r[3] = ((t[2] >>> 7) | (t[3] << 9)) & 0x1fff; 215 | ctx.r[4] = ((t[3] >>> 4) | (t[4] << 12)) & 0x00ff; 216 | ctx.r[5] = (t[4] >>> 1) & 0x1ffe; 217 | ctx.r[6] = ((t[4] >>> 14) | (t[5] << 2)) & 0x1fff; 218 | ctx.r[7] = ((t[5] >>> 11) | (t[6] << 5)) & 0x1f81; 219 | ctx.r[8] = ((t[6] >>> 8) | (t[7] << 8)) & 0x1fff; 220 | ctx.r[9] = (t[7] >>> 5) & 0x007f; 221 | 222 | for (i = 8; i--;) { 223 | ctx.h[i] = 0; 224 | ctx.pad[i] = U8TO16(key, 16+(2*i)); 225 | } 226 | ctx.h[8] = 0; 227 | ctx.h[9] = 0; 228 | ctx.leftover = 0; 229 | ctx.finished = 0; 230 | } 231 | 232 | function poly1305_blocks(ctx, m, mpos, bytes) { 233 | var hibit = ctx.finished ? 0 : (1 << 11); 234 | var t = [], d = [], c = 0, i = 0, j = 0; 235 | 236 | while (bytes >= Poly1305TagSize) { 237 | for (i = 8; i--;) t[i] = U8TO16(m, i*2+mpos); 238 | 239 | ctx.h[0] += t[0] & 0x1fff; 240 | ctx.h[1] += ((t[0] >>> 13) | (t[1] << 3)) & 0x1fff; 241 | ctx.h[2] += ((t[1] >>> 10) | (t[2] << 6)) & 0x1fff; 242 | ctx.h[3] += ((t[2] >>> 7) | (t[3] << 9)) & 0x1fff; 243 | ctx.h[4] += ((t[3] >>> 4) | (t[4] << 12)) & 0x1fff; 244 | ctx.h[5] += (t[4] >>> 1) & 0x1fff; 245 | ctx.h[6] += ((t[4] >>> 14) | (t[5] << 2)) & 0x1fff; 246 | ctx.h[7] += ((t[5] >>> 11) | (t[6] << 5)) & 0x1fff; 247 | ctx.h[8] += ((t[6] >>> 8) | (t[7] << 8)) & 0x1fff; 248 | ctx.h[9] += (t[7] >>> 5) | hibit; 249 | 250 | for (i = 0, c = 0; i < 10; i++) { 251 | d[i] = c; 252 | for (j = 0; j < 10; j++) { 253 | d[i] += (ctx.h[j] & 0xffffffff) * ((j <= i) ? ctx.r[i-j] : (5 * ctx.r[i+10-j])); 254 | if (j === 4) { 255 | c = (d[i] >>> 13); 256 | d[i] &= 0x1fff; 257 | } 258 | } 259 | c += (d[i] >>> 13); 260 | d[i] &= 0x1fff; 261 | } 262 | c = ((c << 2) + c); 263 | c += d[0]; 264 | d[0] = ((c & 0xffff) & 0x1fff); 265 | c = (c >>> 13); 266 | d[1] += c; 267 | 268 | for (i = 10; i--;) ctx.h[i] = d[i] & 0xffff; 269 | 270 | mpos += Poly1305TagSize; 271 | bytes -= Poly1305TagSize; 272 | } 273 | } 274 | 275 | function poly1305_update(ctx, m, bytes) { 276 | var want = 0, i = 0, mpos = 0; 277 | 278 | if (ctx.leftover) { 279 | want = (Poly1305TagSize - ctx.leftover); 280 | if (want > bytes) 281 | want = bytes; 282 | for (i = want; i--;) { 283 | ctx.buffer[ctx.leftover+i] = m[i+mpos]; 284 | } 285 | bytes -= want; 286 | mpos += want; 287 | ctx.leftover += want; 288 | if (ctx.leftover < Poly1305TagSize) 289 | return; 290 | poly1305_blocks(ctx, ctx.buffer, 0, Poly1305TagSize); 291 | ctx.leftover = 0; 292 | } 293 | 294 | if (bytes >= Poly1305TagSize) { 295 | want = (bytes & ~(Poly1305TagSize - 1)); 296 | poly1305_blocks(ctx, m, mpos, want); 297 | mpos += want; 298 | bytes -= want; 299 | } 300 | 301 | if (bytes) { 302 | for (i = bytes; i--;) { 303 | ctx.buffer[ctx.leftover+i] = m[i+mpos]; 304 | } 305 | ctx.leftover += bytes; 306 | } 307 | } 308 | 309 | function poly1305_finish(ctx, mac) { 310 | var g = [], c = 0, mask = 0, f = 0, i = 0; 311 | 312 | if (ctx.leftover) { 313 | i = ctx.leftover; 314 | ctx.buffer[i++] = 1; 315 | for (; i < Poly1305TagSize; i++) { 316 | ctx.buffer[i] = 0; 317 | } 318 | ctx.finished = 1; 319 | poly1305_blocks(ctx, ctx.buffer, 0, Poly1305TagSize); 320 | } 321 | 322 | c = ctx.h[1] >>> 13; 323 | ctx.h[1] &= 0x1fff; 324 | for (i = 2; i < 10; i++) { 325 | ctx.h[i] += c; 326 | c = ctx.h[i] >>> 13; 327 | ctx.h[i] &= 0x1fff; 328 | } 329 | ctx.h[0] += (c * 5); 330 | c = ctx.h[0] >>> 13; 331 | ctx.h[0] &= 0x1fff; 332 | ctx.h[1] += c; 333 | c = ctx.h[1] >>> 13; 334 | ctx.h[1] &= 0x1fff; 335 | ctx.h[2] += c; 336 | 337 | g[0] = ctx.h[0] + 5; 338 | c = g[0] >>> 13; 339 | g[0] &= 0x1fff; 340 | for (i = 1; i < 10; i++) { 341 | g[i] = ctx.h[i] + c; 342 | c = g[i] >>> 13; 343 | g[i] &= 0x1fff; 344 | } 345 | g[9] -= (1 << 13); 346 | g[9] &= 0xffff; 347 | 348 | mask = (g[9] >>> 15) - 1; 349 | for (i = 10; i--;) g[i] &= mask; 350 | mask = ~mask; 351 | for (i = 10; i--;) { 352 | ctx.h[i] = (ctx.h[i] & mask) | g[i]; 353 | } 354 | 355 | ctx.h[0] = ((ctx.h[0] ) | (ctx.h[1] << 13)) & 0xffff; 356 | ctx.h[1] = ((ctx.h[1] >> 3) | (ctx.h[2] << 10)) & 0xffff; 357 | ctx.h[2] = ((ctx.h[2] >> 6) | (ctx.h[3] << 7)) & 0xffff; 358 | ctx.h[3] = ((ctx.h[3] >> 9) | (ctx.h[4] << 4)) & 0xffff; 359 | ctx.h[4] = ((ctx.h[4] >> 12) | (ctx.h[5] << 1) | (ctx.h[6] << 14)) & 0xffff; 360 | ctx.h[5] = ((ctx.h[6] >> 2) | (ctx.h[7] << 11)) & 0xffff; 361 | ctx.h[6] = ((ctx.h[7] >> 5) | (ctx.h[8] << 8)) & 0xffff; 362 | ctx.h[7] = ((ctx.h[8] >> 8) | (ctx.h[9] << 5)) & 0xffff; 363 | 364 | f = (ctx.h[0] & 0xffffffff) + ctx.pad[0]; 365 | ctx.h[0] = f & 0xffff; 366 | for (i = 1; i < 8; i++) { 367 | f = (ctx.h[i] & 0xffffffff) + ctx.pad[i] + (f >>> 16); 368 | ctx.h[i] = f & 0xffff; 369 | } 370 | 371 | for (i = 8; i--;) { 372 | U16TO8(mac, i*2, ctx.h[i]); 373 | ctx.pad[i] = 0; 374 | } 375 | for (i = 10; i--;) { 376 | ctx.h[i] = 0; 377 | ctx.r[i] = 0; 378 | } 379 | } 380 | 381 | function poly1305_auth(mac, m, bytes, key) { 382 | var ctx = new Poly1305Ctx(); 383 | poly1305_init(ctx, key); 384 | poly1305_update(ctx, m, bytes); 385 | poly1305_finish(ctx, mac); 386 | } 387 | 388 | function poly1305_verify(mac1, mac2) { 389 | var dif = 0; 390 | for (var i = 0; i < 16; i++) { 391 | dif |= (mac1[i] ^ mac2[i]); 392 | } 393 | dif = (dif - 1) >>> 31; 394 | return (dif & 1); 395 | } 396 | 397 | /* chacha20poly1305 AEAD */ 398 | 399 | // Written in 2014 by Devi Mandiri. Public domain. 400 | 401 | // Caveat: 402 | // http://tools.ietf.org/html/draft-agl-tls-chacha20poly1305-04#page-9 403 | // specified P_MAX and A_MAX are 2^64 and C_MAX is 2^64+16. 404 | // While according to http://www.ecma-international.org/ecma-262/5.1/#sec-15.4 405 | // I think max input length = 2^32-1 = 4294967295 = ~3.9Gb due to the ToUint32 abstract operation. 406 | // Whatever ;) 407 | 408 | function AeadCtx(key) { 409 | this.key = key; 410 | }; 411 | 412 | function aead_init(c20ctx, key, nonce) { 413 | chacha20_keysetup(c20ctx, key); 414 | chacha20_ivsetup(c20ctx, nonce); 415 | 416 | var subkey = []; 417 | chacha20_keystream(c20ctx, subkey, 64); 418 | 419 | return subkey.slice(0, 32); 420 | } 421 | 422 | function store64(dst, pos, num) { 423 | var hi = 0, lo = num >>> 0; 424 | if ((+(Math.abs(num))) >= 1) { 425 | if (num > 0) { 426 | hi = ((Math.min((+(Math.floor(num/4294967296))), 4294967295))|0) >>> 0; 427 | } else { 428 | hi = (~~((+(Math.ceil((num - +(((~~(num)))>>>0))/4294967296))))) >>> 0; 429 | } 430 | } 431 | dst[pos] = lo & 0xff; lo >>>= 8; 432 | dst[pos+1] = lo & 0xff; lo >>>= 8; 433 | dst[pos+2] = lo & 0xff; lo >>>= 8; 434 | dst[pos+3] = lo & 0xff; 435 | dst[pos+4] = hi & 0xff; hi >>>= 8; 436 | dst[pos+5] = hi & 0xff; hi >>>= 8; 437 | dst[pos+6] = hi & 0xff; hi >>>= 8; 438 | dst[pos+7] = hi & 0xff; 439 | } 440 | 441 | function aead_mac(key, ciphertext, data) { 442 | var clen = ciphertext.length; 443 | var dlen = data.length; 444 | var m = new Array(clen + dlen + 16); 445 | var i = dlen; 446 | 447 | for (; i--;) m[i] = data[i]; 448 | store64(m, dlen, dlen); 449 | 450 | for (i = clen; i--;) m[dlen+8+i] = ciphertext[i]; 451 | store64(m, clen+dlen+8, clen); 452 | 453 | var mac = []; 454 | poly1305_auth(mac, m, m.length, key); 455 | 456 | return mac; 457 | } 458 | 459 | function aead_encrypt(ctx, nonce, input, ad) { 460 | var c = new Chacha20Ctx(); 461 | var key = aead_init(c, ctx.key, nonce); 462 | 463 | var ciphertext = []; 464 | chacha20_encrypt(c, ciphertext, input, input.length); 465 | 466 | var mac = aead_mac(key, ciphertext, ad); 467 | 468 | var out = []; 469 | out = out.concat(ciphertext, mac); 470 | 471 | return out; 472 | } 473 | 474 | function aead_decrypt(ctx, nonce, ciphertext, ad) { 475 | var c = new Chacha20Ctx(); 476 | var key = aead_init(c, ctx.key, nonce); 477 | var clen = ciphertext.length - Poly1305TagSize; 478 | var digest = ciphertext.slice(clen); 479 | var mac = aead_mac(key, ciphertext.slice(0, clen), ad); 480 | 481 | if (poly1305_verify(digest, mac) !== 1) return false; 482 | 483 | var out = []; 484 | chacha20_decrypt(c, out, ciphertext, clen); 485 | return out; 486 | } 487 | -------------------------------------------------------------------------------- /lib/util/clone.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | module.exports = { 5 | clone: clone 6 | } 7 | 8 | 9 | /** 10 | * A simple clone function that also allows you to pass an "extend" object whose properties will be 11 | * added to the cloned copy of the original object passed. 12 | */ 13 | function clone(object, extend) { 14 | 15 | var cloned = {}; 16 | 17 | for (var key in object) { 18 | cloned[key] = object[key]; 19 | } 20 | 21 | for (var key in extend) { 22 | cloned[key] = extend[key]; 23 | } 24 | 25 | return cloned; 26 | }; 27 | -------------------------------------------------------------------------------- /lib/util/encryption.js: -------------------------------------------------------------------------------- 1 | var crypto = require("crypto"); 2 | var chacha20poly1305 = require("./chacha20poly1305"); 3 | var curve25519 = require("curve25519"); 4 | var assert = require('assert'); 5 | 6 | module.exports = { 7 | generateCurve25519SecretKey: generateCurve25519SecretKey, 8 | generateCurve25519PublicKeyFromSecretKey: generateCurve25519PublicKeyFromSecretKey, 9 | generateCurve25519SharedSecKey: generateCurve25519SharedSecKey, 10 | layerEncrypt: layerEncrypt, 11 | layerDecrypt: layerDecrypt, 12 | verifyAndDecrypt: verifyAndDecrypt, 13 | encryptAndSeal: encryptAndSeal 14 | }; 15 | 16 | function fromHex(h) { 17 | h.replace(/([^0-9a-f])/g, ''); 18 | var out = [], len = h.length, w = ''; 19 | for (var i = 0; i < len; i += 2) { 20 | w = h[i]; 21 | if (((i+1) >= len) || typeof h[i+1] === 'undefined') { 22 | w += '0'; 23 | } else { 24 | w += h[i+1]; 25 | } 26 | out.push(parseInt(w, 16)); 27 | } 28 | return out; 29 | } 30 | 31 | function generateCurve25519SecretKey() { 32 | var secretKey = Buffer(32); 33 | curve25519.makeSecretKey(secretKey); 34 | return secretKey; 35 | } 36 | 37 | function generateCurve25519PublicKeyFromSecretKey(secKey) { 38 | var publicKey = curve25519.derivePublicKey(secKey); 39 | return publicKey; 40 | } 41 | 42 | function generateCurve25519SharedSecKey(secKey, pubKey) { 43 | var sharedSec = curve25519.deriveSharedSecret(secKey, pubKey); 44 | return sharedSec; 45 | } 46 | 47 | //Security Layer Enc/Dec 48 | 49 | function layerEncrypt(data, count, key) { 50 | var result = Buffer(0); 51 | var total = data.length; 52 | for (var offset = 0; offset < total; ) { 53 | var length = Math.min(total - offset, 0x400); 54 | var leLength = Buffer(2); 55 | leLength.writeUInt16LE(length,0); 56 | 57 | var nonce = Buffer(8); 58 | writeUInt64LE(count.value++, nonce, 0); 59 | 60 | var result_Buffer = Buffer(Array(length)); 61 | var result_mac = Buffer(Array(16)); 62 | encryptAndSeal(key, nonce, data.slice(offset, offset + length), 63 | leLength,result_Buffer, result_mac); 64 | 65 | offset += length; 66 | 67 | result = Buffer.concat([result,leLength,result_Buffer,result_mac]); 68 | } 69 | return result; 70 | } 71 | 72 | function layerDecrypt(packet, count, key) { 73 | var result = Buffer(0); 74 | var total = packet.length; 75 | 76 | for (var offset = 0; offset < total;) { 77 | var realDataLength = packet.slice(offset,offset+2).readUInt16LE(0); 78 | 79 | var nonce = Buffer(8); 80 | writeUInt64LE(count.value++, nonce, 0); 81 | 82 | var result_Buffer = Buffer(Array(realDataLength)); 83 | 84 | if (verifyAndDecrypt(key, nonce, packet.slice(offset + 2, offset + 2 + realDataLength), 85 | packet.slice(offset + 2 + realDataLength, offset + 2 + realDataLength + 16), 86 | packet.slice(offset,offset+2),result_Buffer)) { 87 | result = Buffer.concat([result,result_Buffer]); 88 | offset += (18 + realDataLength); 89 | } else { 90 | console.log("Layer Decrypt fail!"); 91 | return 0; 92 | } 93 | }; 94 | 95 | return result; 96 | } 97 | 98 | //General Enc/Dec 99 | function verifyAndDecrypt(key,nonce,ciphertext,mac,addData,plaintext) { 100 | var ctx = new chacha20poly1305.Chacha20Ctx(); 101 | chacha20poly1305.chacha20_keysetup(ctx, key); 102 | chacha20poly1305.chacha20_ivsetup(ctx, nonce); 103 | var poly1305key = Buffer(64); 104 | var zeros = Buffer(Array(64)); 105 | chacha20poly1305.chacha20_update(ctx,poly1305key,zeros,zeros.length); 106 | 107 | var poly1305_contxt = new chacha20poly1305.Poly1305Ctx(); 108 | chacha20poly1305.poly1305_init(poly1305_contxt, poly1305key); 109 | 110 | var addDataLength = 0; 111 | if (addData != undefined) { 112 | addDataLength = addData.length; 113 | chacha20poly1305.poly1305_update(poly1305_contxt, addData, addData.length); 114 | if ((addData.length % 16) != 0) { 115 | chacha20poly1305.poly1305_update(poly1305_contxt, Buffer(Array(16-(addData.length%16))), 16-(addData.length%16)); 116 | } 117 | } 118 | 119 | chacha20poly1305.poly1305_update(poly1305_contxt, ciphertext, ciphertext.length); 120 | if ((ciphertext.length % 16) != 0) { 121 | chacha20poly1305.poly1305_update(poly1305_contxt, Buffer(Array(16-(ciphertext.length%16))), 16-(ciphertext.length%16)); 122 | } 123 | 124 | var leAddDataLen = Buffer(8); 125 | writeUInt64LE(addDataLength, leAddDataLen, 0); 126 | chacha20poly1305.poly1305_update(poly1305_contxt, leAddDataLen, 8); 127 | 128 | var leTextDataLen = Buffer(8); 129 | writeUInt64LE(ciphertext.length, leTextDataLen, 0); 130 | chacha20poly1305.poly1305_update(poly1305_contxt, leTextDataLen, 8); 131 | 132 | var poly_out = []; 133 | chacha20poly1305.poly1305_finish(poly1305_contxt, poly_out); 134 | 135 | if (chacha20poly1305.poly1305_verify(mac, poly_out) != 1) { 136 | console.log("Verify Fail"); 137 | return false; 138 | } else { 139 | var written = chacha20poly1305.chacha20_update(ctx,plaintext,ciphertext,ciphertext.length); 140 | chacha20poly1305.chacha20_final(ctx,plaintext.slice(written, ciphertext.length)); 141 | return true; 142 | } 143 | } 144 | 145 | function encryptAndSeal(key,nonce,plaintext,addData,ciphertext,mac) { 146 | var ctx = new chacha20poly1305.Chacha20Ctx(); 147 | chacha20poly1305.chacha20_keysetup(ctx, key); 148 | chacha20poly1305.chacha20_ivsetup(ctx, nonce); 149 | var poly1305key = Buffer(64); 150 | var zeros = Buffer(Array(64)); 151 | chacha20poly1305.chacha20_update(ctx,poly1305key,zeros,zeros.length); 152 | 153 | var written = chacha20poly1305.chacha20_update(ctx,ciphertext,plaintext,plaintext.length); 154 | chacha20poly1305.chacha20_final(ctx,ciphertext.slice(written,plaintext.length)); 155 | 156 | var poly1305_contxt = new chacha20poly1305.Poly1305Ctx(); 157 | chacha20poly1305.poly1305_init(poly1305_contxt, poly1305key); 158 | 159 | var addDataLength = 0; 160 | if (addData != undefined) { 161 | addDataLength = addData.length; 162 | chacha20poly1305.poly1305_update(poly1305_contxt, addData, addData.length); 163 | if ((addData.length % 16) != 0) { 164 | chacha20poly1305.poly1305_update(poly1305_contxt, Buffer(Array(16-(addData.length%16))), 16-(addData.length%16)); 165 | } 166 | } 167 | 168 | chacha20poly1305.poly1305_update(poly1305_contxt, ciphertext, ciphertext.length); 169 | if ((ciphertext.length % 16) != 0) { 170 | chacha20poly1305.poly1305_update(poly1305_contxt, Buffer(Array(16-(ciphertext.length%16))), 16-(ciphertext.length%16)); 171 | } 172 | 173 | var leAddDataLen = Buffer(8); 174 | writeUInt64LE(addDataLength, leAddDataLen, 0); 175 | chacha20poly1305.poly1305_update(poly1305_contxt, leAddDataLen, 8); 176 | 177 | var leTextDataLen = Buffer(8); 178 | writeUInt64LE(ciphertext.length, leTextDataLen, 0); 179 | chacha20poly1305.poly1305_update(poly1305_contxt, leTextDataLen, 8); 180 | 181 | chacha20poly1305.poly1305_finish(poly1305_contxt, mac); 182 | } 183 | 184 | var MAX_UINT32 = 0x00000000FFFFFFFF 185 | var MAX_INT53 = 0x001FFFFFFFFFFFFF 186 | 187 | function onesComplement(number) { 188 | number = ~number 189 | if (number < 0) { 190 | number = (number & 0x7FFFFFFF) + 0x80000000 191 | } 192 | return number 193 | } 194 | 195 | function uintHighLow(number) { 196 | assert(number > -1 && number <= MAX_INT53, "number out of range") 197 | assert(Math.floor(number) === number, "number must be an integer") 198 | var high = 0 199 | var signbit = number & 0xFFFFFFFF 200 | var low = signbit < 0 ? (number & 0x7FFFFFFF) + 0x80000000 : signbit 201 | if (number > MAX_UINT32) { 202 | high = (number - low) / (MAX_UINT32 + 1) 203 | } 204 | return [high, low] 205 | } 206 | 207 | function intHighLow(number) { 208 | if (number > -1) { 209 | return uintHighLow(number) 210 | } 211 | var hl = uintHighLow(-number) 212 | var high = onesComplement(hl[0]) 213 | var low = onesComplement(hl[1]) 214 | if (low === MAX_UINT32) { 215 | high += 1 216 | low = 0 217 | } 218 | else { 219 | low += 1 220 | } 221 | return [high, low] 222 | } 223 | 224 | function writeUInt64BE(number, buffer, offset) { 225 | offset = offset || 0 226 | var hl = uintHighLow(number) 227 | buffer.writeUInt32BE(hl[0], offset) 228 | buffer.writeUInt32BE(hl[1], offset + 4) 229 | } 230 | 231 | function writeUInt64LE (number, buffer, offset) { 232 | offset = offset || 0 233 | var hl = uintHighLow(number) 234 | buffer.writeUInt32LE(hl[1], offset) 235 | buffer.writeUInt32LE(hl[0], offset + 4) 236 | } 237 | -------------------------------------------------------------------------------- /lib/util/eventedhttp.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('EventedHTTPServer'); 2 | var EventEmitter = require('events').EventEmitter; 3 | var inherits = require('util').inherits; 4 | var net = require('net'); 5 | var http = require('http'); 6 | 7 | 'use strict'; 8 | 9 | module.exports = { 10 | EventedHTTPServer: EventedHTTPServer 11 | } 12 | 13 | 14 | /** 15 | * EventedHTTPServer provides an HTTP-like server that supports HAP "extensions" for security and events. 16 | * 17 | * Implementation 18 | * -------------- 19 | * In order to implement the "custom HTTP" server required by the HAP protocol (see HAPServer.js) without completely 20 | * reinventing the wheel, we create both a generic TCP socket server as well as a standard Node HTTP server. 21 | * The TCP socket server acts as a proxy, allowing users of this class to transform data (for encryption) as necessary 22 | * and passing through bytes directly to the HTTP server for processing. This way we get Node to do all 23 | * the "heavy lifting" of HTTP like parsing headers and formatting responses. 24 | * 25 | * Events are sent by simply waiting for current HTTP traffic to subside and then sending a custom response packet 26 | * directly down the wire via the socket. 27 | * 28 | * Each connection to the main TCP server gets its own internal HTTP server, so we can track ongoing requests/responses 29 | * for safe event insertion. 30 | * 31 | * @event 'listening' => function() { } 32 | * Emitted when the server is fully set up and ready to receive connections. 33 | * 34 | * @event 'request' => function(request, response, session, events) { } 35 | * Just like the 'http' module, request is http.IncomingMessage and response is http.ServerResponse. 36 | * The 'session' param is an arbitrary object that you can use to store data associated with this connection; 37 | * it will not be used by this class. The 'events' param is an object where the keys are the names of 38 | * events that this connection has signed up for. It is initially empty and listeners are expected to manage it. 39 | * 40 | * @event 'decrypt' => function(data, {decrypted.data}, session) { } 41 | * Fired when we receive data from the client device. You may detemine whether the data is encrypted, and if 42 | * so, you can decrypt the data and store it into a new 'data' property of the 'decrypted' argument. If data is not 43 | * encrypted, you can simply leave 'data' as null and the original data will be passed through as-is. 44 | * 45 | * @event 'encrypt' => function(data, {encrypted.data}, session) { } 46 | * Fired when we wish to send data to the client device. If necessary, set the 'data' property of the 47 | * 'encrypted' argument to be the encrypted data and it will be sent instead. 48 | */ 49 | 50 | function EventedHTTPServer() { 51 | this._tcpServer = net.createServer(); 52 | this._connections = []; // track all open connections (for sending events) 53 | } 54 | 55 | inherits(EventedHTTPServer, EventEmitter); 56 | 57 | EventedHTTPServer.prototype.listen = function(port) { 58 | this._tcpServer.listen(port); 59 | 60 | this._tcpServer.on('listening', function() { 61 | debug("Server listening on port %s", port); 62 | this.emit('listening'); 63 | }.bind(this)); 64 | 65 | this._tcpServer.on('connection', this._onConnection.bind(this)); 66 | } 67 | 68 | EventedHTTPServer.prototype.sendEvent = function(event, data, contentType, exclude) { 69 | for (var index in this._connections) { 70 | var connection = this._connections[index]; 71 | connection.sendEvent(event, data, contentType, exclude); 72 | } 73 | } 74 | 75 | // Called by net.Server when a new client connects. We will set up a new EventedHTTPServerConnection to manage the 76 | // lifetime of this connection. 77 | EventedHTTPServer.prototype._onConnection = function(socket) { 78 | 79 | var connection = new EventedHTTPServerConnection(socket); 80 | 81 | // pass on session events to our listeners directly 82 | connection.on('request', function(request, response, session, events) { this.emit('request', request, response, session, events); }.bind(this)); 83 | connection.on('encrypt', function(data, encrypted, session) { this.emit('encrypt', data, encrypted, session); }.bind(this)); 84 | connection.on('decrypt', function(data, decrypted, session) { this.emit('decrypt', data, decrypted, session); }.bind(this)); 85 | connection.on('close', function() { this._handleConnectionClose(connection); }.bind(this)); 86 | this._connections.push(connection); 87 | } 88 | 89 | EventedHTTPServer.prototype._handleConnectionClose = function(connection) { 90 | 91 | // remove it from our array of connections for events 92 | this._connections.splice(this._connections.indexOf(connection), 1); 93 | } 94 | 95 | 96 | /** 97 | * Manages a single iOS-initiated HTTP connection during its lifetime. 98 | * 99 | * @event 'request' => function(request, response) { } 100 | * @event 'decrypt' => function(data, {decrypted.data}, session) { } 101 | * @event 'encrypt' => function(data, {encrypted.data}, session) { } 102 | * @event 'close' => function() { } 103 | */ 104 | 105 | function EventedHTTPServerConnection(clientSocket) { 106 | this._remoteAddress = clientSocket.remoteAddress; // cache because it becomes undefined in 'onClientSocketClose' 107 | this._pendingClientSocketData = Buffer(0); // data received from client before HTTP proxy is fully setup 108 | this._fullySetup = false; // true when we are finished establishing connections 109 | this._writingResponse = false; // true while we are composing an HTTP response (so events can wait) 110 | this._pendingEventData = Buffer(0); // event data waiting to be sent until after an in-progress HTTP response is being written 111 | 112 | // clientSocket is the socket connected to the actual iOS device 113 | this._clientSocket = clientSocket; 114 | this._clientSocket.on('data', this._onClientSocketData.bind(this)); 115 | this._clientSocket.on('close', this._onClientSocketClose.bind(this)); 116 | this._clientSocket.on('error', this._onClientSocketError.bind(this)); // we MUST register for this event, otherwise the error will bubble up to the top and crash the node process entirely. 117 | 118 | // serverSocket is our connection to our own internal httpServer 119 | this._serverSocket = null; // created after httpServer 'listening' event 120 | 121 | // create our internal HTTP server for this connection that we will proxy data to and from 122 | this._httpServer = http.createServer(); 123 | this._httpServer.timeout = 0; // clients expect to hold connections open as long as they want 124 | this._httpServer.on('listening', this._onHttpServerListening.bind(this)); 125 | this._httpServer.on('request', this._onHttpServerRequest.bind(this)); 126 | this._httpServer.on('error', this._onHttpServerError.bind(this)); 127 | this._httpServer.listen(0); 128 | 129 | // an arbitrary dict that users of this class can store values in to associate with this particular connection 130 | this._session = {}; 131 | 132 | // a collection of event names subscribed to by this connection 133 | this._events = {}; // this._events[eventName] = true (value is arbitrary, but must be truthy) 134 | 135 | debug("[%s] New connection from client", this._remoteAddress); 136 | } 137 | 138 | inherits(EventedHTTPServerConnection, EventEmitter); 139 | 140 | EventedHTTPServerConnection.prototype.sendEvent = function(event, data, contentType, excludeEvents) { 141 | 142 | // has this connection subscribed to the given event? if not, nothing to do! 143 | if (!this._events[event]) { 144 | return; 145 | } 146 | 147 | // does this connection's 'events' object match the excludeEvents object? if so, don't send the event. 148 | if (excludeEvents === this._events) { 149 | debug("[%s] Muting event '%s' notification for this connection since it originated here.", this._remoteAddress, event); 150 | return; 151 | } 152 | 153 | debug("[%s] Sending HTTP event '%s' with data: %s", this._remoteAddress, event, data.toString('utf8')); 154 | 155 | // ensure data is a Buffer 156 | data = Buffer(data); 157 | 158 | // format this payload as an HTTP response 159 | var linebreak = new Buffer("0D0A","hex"); 160 | data = Buffer.concat([ 161 | new Buffer('EVENT/1.0 200 OK'), linebreak, 162 | new Buffer('Content-Type: ' + contentType), linebreak, 163 | new Buffer('Content-Length: ' + data.length), linebreak, 164 | linebreak, 165 | data 166 | ]); 167 | 168 | // give listeners an opportunity to encrypt this data before sending it to the client 169 | var encrypted = { data: null }; 170 | this.emit('encrypt', data, encrypted, this._session); 171 | if (encrypted.data) data = encrypted.data; 172 | 173 | // if we're in the middle of writing an HTTP response already, put this event in the queue for when 174 | // we're done. otherwise send it immediately. 175 | if (this._writingResponse) 176 | this._pendingEventData = Buffer.concat([this._pendingEventData, data]); 177 | else 178 | this._clientSocket.write(data); 179 | } 180 | 181 | EventedHTTPServerConnection.prototype._sendPendingEvents = function() { 182 | 183 | // an existing HTTP response was finished, so let's flush our pending event buffer if necessary! 184 | if (this._pendingEventData.length > 0) { 185 | debug("[%s] Writing pending HTTP event data", this._remoteAddress); 186 | this._clientSocket.write(this._pendingEventData); 187 | } 188 | 189 | // clear the buffer 190 | this._pendingEventData = new Buffer(0); 191 | } 192 | 193 | // Called only once right after constructor finishes 194 | EventedHTTPServerConnection.prototype._onHttpServerListening = function() { 195 | this._httpPort = this._httpServer.address().port; 196 | debug("[%s] HTTP server listening on port %s", this._remoteAddress, this._httpPort); 197 | 198 | // closes before this are due to retrying listening, which don't need to be handled 199 | this._httpServer.on('close', this._onHttpServerClose.bind(this)); 200 | 201 | // now we can establish a connection to this running HTTP server for proxying data 202 | this._serverSocket = net.createConnection(this._httpPort); 203 | this._serverSocket.on('connect', this._onServerSocketConnect.bind(this)); 204 | this._serverSocket.on('data', this._onServerSocketData.bind(this)); 205 | this._serverSocket.on('close', this._onServerSocketClose.bind(this)); 206 | this._serverSocket.on('error', this._onServerSocketError.bind(this)); // we MUST register for this event, otherwise the error will bubble up to the top and crash the node process entirely. 207 | } 208 | 209 | // Called only once right after onHttpServerListening 210 | EventedHTTPServerConnection.prototype._onServerSocketConnect = function() { 211 | 212 | // we are now fully set up: 213 | // - clientSocket is connected to the iOS device 214 | // - serverSocket is connected to the httpServer 215 | // - ready to proxy data! 216 | this._fullySetup = true; 217 | 218 | // start by flushing any pending buffered data received from the client while we were setting up 219 | if (this._pendingClientSocketData.length > 0) { 220 | this._serverSocket.write(this._pendingClientSocketData); 221 | this._pendingClientSocketData = null; 222 | } 223 | } 224 | 225 | // Received data from client (iOS) 226 | EventedHTTPServerConnection.prototype._onClientSocketData = function(data) { 227 | 228 | // give listeners an opportunity to decrypt this data before processing it as HTTP 229 | var decrypted = { data: null }; 230 | this.emit('decrypt', data, decrypted, this._session); 231 | if (decrypted.data) data = decrypted.data; 232 | 233 | if (this._fullySetup) { 234 | // proxy it along to the HTTP server 235 | this._serverSocket.write(data); 236 | } 237 | else { 238 | // we're not setup yet, so add this data to our buffer 239 | this._pendingClientSocketData = Buffer.concat([this._pendingClientSocketData, data]); 240 | } 241 | } 242 | 243 | // Received data from HTTP Server 244 | EventedHTTPServerConnection.prototype._onServerSocketData = function(data) { 245 | 246 | // give listeners an opportunity to encrypt this data before sending it to the client 247 | var encrypted = { data: null }; 248 | this.emit('encrypt', data, encrypted, this._session); 249 | if (encrypted.data) data = encrypted.data; 250 | 251 | // proxy it along to the client (iOS) 252 | this._clientSocket.write(data); 253 | } 254 | 255 | // Our internal HTTP Server has been closed (happens after we call this._httpServer.close() below) 256 | EventedHTTPServerConnection.prototype._onServerSocketClose = function() { 257 | debug("[%s] HTTP connection was closed", this._remoteAddress); 258 | 259 | // make sure the iOS side is closed as well 260 | this._clientSocket.destroy(); 261 | 262 | // we only support a single long-lived connection to our internal HTTP server. Since it's closed, 263 | // we'll need to shut it down entirely. 264 | this._httpServer.close(); 265 | } 266 | 267 | // Our internal HTTP Server has been closed (happens after we call this._httpServer.close() below) 268 | EventedHTTPServerConnection.prototype._onServerSocketError = function(err) { 269 | debug("[%s] HTTP connection error: ", this._remoteAddress, err.message); 270 | 271 | // _onServerSocketClose will be called next 272 | } 273 | 274 | EventedHTTPServerConnection.prototype._onHttpServerRequest = function(request, response) { 275 | debug("[%s] HTTP request: %s", this._remoteAddress, request.url); 276 | 277 | this._writingResponse = true; 278 | 279 | // sign up to know when the response is ended, so we can safely send EVENT responses 280 | response.on('finish', function() { 281 | 282 | debug("[%s] HTTP Response is finished", this._remoteAddress); 283 | this._writingResponse = false; 284 | this._sendPendingEvents(); 285 | 286 | }.bind(this)); 287 | 288 | // pass it along to listeners 289 | this.emit('request', request, response, this._session, this._events); 290 | } 291 | 292 | EventedHTTPServerConnection.prototype._onHttpServerClose = function() { 293 | debug("[%s] HTTP server was closed", this._remoteAddress); 294 | 295 | // notify listeners that we are completely closed 296 | this.emit('close'); 297 | } 298 | 299 | EventedHTTPServerConnection.prototype._onHttpServerError = function(err) { 300 | debug("[%s] HTTP server error: %s", this._remoteAddress, err.message); 301 | 302 | if (err.code === 'EADDRINUSE') { 303 | this._httpServer.close(); 304 | this._httpServer.listen(0); 305 | } 306 | } 307 | 308 | EventedHTTPServerConnection.prototype._onClientSocketClose = function() { 309 | debug("[%s] Client connection closed", this._remoteAddress); 310 | 311 | // shutdown the other side 312 | this._serverSocket.destroy(); 313 | } 314 | 315 | EventedHTTPServerConnection.prototype._onClientSocketError = function(err) { 316 | debug("[%s] Client connection error: %s", this._remoteAddress, err.message); 317 | 318 | // _onClientSocketClose will be called next 319 | } 320 | -------------------------------------------------------------------------------- /lib/util/hkdf.js: -------------------------------------------------------------------------------- 1 | var crypto = require("crypto"); 2 | 3 | module.exports = { 4 | HKDF: HKDF 5 | }; 6 | 7 | function HKDF(hashAlg, salt, ikm, info, size) { 8 | // create the hash alg to see if it exists and get its length 9 | var hash = crypto.createHash(hashAlg); 10 | var hashLength = hash.digest().length; 11 | 12 | // now we compute the PRK 13 | var hmac = crypto.createHmac(hashAlg, salt); 14 | hmac.update(ikm); 15 | var prk = hmac.digest(); 16 | 17 | var prev = new Buffer(0); 18 | var output; 19 | var buffers = []; 20 | var num_blocks = Math.ceil(size / hashLength); 21 | info = new Buffer(info); 22 | 23 | for (var i=0; i 0;) { 30 | if (leftLength >= 255) { 31 | tempBuffer = Buffer.concat([tempBuffer,Buffer([type,0xFF]),data.slice(currentStart, currentStart + 255)]); 32 | leftLength -= 255; 33 | currentStart = currentStart + 255; 34 | } else { 35 | tempBuffer = Buffer.concat([tempBuffer,Buffer([type,leftLength]),data.slice(currentStart, currentStart + leftLength)]); 36 | leftLength -= leftLength; 37 | } 38 | }; 39 | 40 | encodedTLVBuffer = tempBuffer; 41 | } 42 | 43 | // do we have more to encode? 44 | if (arguments.length > 2) { 45 | 46 | // chop off the first two arguments which we already processed, and process the rest recursively 47 | var remainingArguments = Array.prototype.slice.call(arguments, 2); 48 | var remainingTLVBuffer = encode.apply(this, remainingArguments); 49 | 50 | // append the remaining encoded arguments directly to the buffer 51 | encodedTLVBuffer = Buffer.concat([encodedTLVBuffer, remainingTLVBuffer]) 52 | } 53 | 54 | return encodedTLVBuffer; 55 | } 56 | 57 | function decode(data) { 58 | 59 | var objects = {}; 60 | 61 | var leftLength = data.length; 62 | var currentIndex = 0; 63 | 64 | for (; leftLength > 0;) { 65 | var type = data[currentIndex] 66 | var length = data[currentIndex+1] 67 | currentIndex += 2; 68 | leftLength -= 2; 69 | 70 | var newData = data.slice(currentIndex, currentIndex+length); 71 | 72 | if (objects[type]) { 73 | objects[type] = Buffer.concat([objects[type],newData]); 74 | } else { 75 | objects[type] = newData; 76 | } 77 | 78 | currentIndex += length; 79 | leftLength -= length; 80 | }; 81 | 82 | return objects; 83 | } -------------------------------------------------------------------------------- /lib/util/uuid.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | 3 | 'use strict'; 4 | 5 | module.exports = { 6 | generate: generate, 7 | isValid: isValid 8 | } 9 | 10 | // http://stackoverflow.com/a/25951500/66673 11 | function generate(data) { 12 | var sha1sum = crypto.createHash('sha1'); 13 | sha1sum.update(data); 14 | var s = sha1sum.digest('hex'); 15 | var i = -1; 16 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 17 | i += 1; 18 | switch (c) { 19 | case 'x': 20 | return s[i]; 21 | case 'y': 22 | return ((parseInt('0x' + s[i], 16) & 0x3) | 0x8).toString(16); 23 | } 24 | }); 25 | }; 26 | 27 | var validUUIDRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i 28 | 29 | function isValid(UUID) { 30 | return validUUIDRegex.test(UUID); 31 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hap-nodejs", 3 | "version": "0.3.0", 4 | "description": "HAP-NodeJS is a Node.js implementation of HomeKit Accessory Server.", 5 | "main": "index.js", 6 | "dependencies": { 7 | "curve25519": "git://github.com/KhaosT/node-curve25519", 8 | "debug": "^2.2.0", 9 | "ed25519": "git://github.com/KhaosT/ed25519", 10 | "mdns": "git://github.com/KhaosT/node_mdns", 11 | "node-persist": "^0.0.8", 12 | "srp": "git://github.com/KhaosT/node-srp" 13 | }, 14 | "devDependencies": { 15 | "simple-plist": "0.0.4" 16 | }, 17 | "scripts": { 18 | "test": "echo \"Error: no test specified\" && exit 1", 19 | "start": "node BridgedCore.js" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/KhaosT/HAP-NodeJS.git" 24 | }, 25 | "author": "Khaos Tian (http://tz.is/)", 26 | "license": "Apache-2.0", 27 | "bugs": { 28 | "url": "https://github.com/KhaosT/HAP-NodeJS/issues" 29 | }, 30 | "homepage": "https://github.com/KhaosT/HAP-NodeJS" 31 | } 32 | --------------------------------------------------------------------------------