├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── basic-http-setup.js ├── basic-tcp-setup.js └── config.js ├── index.js ├── lib ├── HttpSoftAP.js ├── SoftAP.js └── TcpSoftAP.js ├── package.json └── test ├── .eslintrc ├── SoftAP.spec.js └── index.js /.eslintignore: -------------------------------------------------------------------------------- 1 | softap-browser.js 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "particle", 3 | "env": { 4 | "shared-node-browser": true, 5 | "node": true 6 | }, 7 | "rules": { 8 | "no-console": 0 9 | } 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | softap-browser.js -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "4" 5 | - "5" 6 | - "6" 7 | - "7" 8 | 9 | sudo: false 10 | 11 | after_success: 12 | - npm run coveralls 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # changelog 2 | 3 | ## 4.1.0 - 30 June 2017 4 | 5 | * Support for WPA Enterprise WiFi setup 6 | * Minimum version of node bumped from 0.10 to 4. 7 | 8 | ## 4.0.3 - 15 Feb 2017 9 | 10 | * Open a dummy socket connection on first TCP command to work around 11 | a firmware [glitch](https://github.com/spark/firmware/issues/1252) 12 | 13 | ## 4.0.2 - 15 Feb 2017 14 | 15 | * [#17](https://github.com/spark/softap-setup-js/pull/17) change license from LGPL to Apache 2.0 16 | * [#13](https://github.com/spark/softap-setup-js/pull/13) default timeout increase from 2s to 8s for TCP 17 | * [#13](https://github.com/spark/softap-setup-js/pull/13) TCP response can be spread over multiple packets 18 | * [#24](https://github.com/spark/softap-setup-js/issues/24) Configuration defaults move into SoftAP class so they are available for TCP and HTTP 19 | * [#18](https://github.com/spark/softap-setup-js/issues/18) Device ID change to lowercase 20 | * [#23](https://github.com/spark/softap-setup-js/issues/23) deviceInfo request was failing with timeout 21 | 22 | 23 | ## 4.0.1 - 26 Feb 2016 24 | 25 | * Explicitly specify 'http' as protocol 26 | 27 | ## 4.0.0 - 23 Feb 2016 28 | 29 | * Remove environment variable and file configuration options. 30 | * Rename `keep_alive` to `keepAlive` and `no_delay` to `noDelay`. 31 | * Implement a strict timeout. 32 | * Improve error handling. 33 | 34 | ## 3.0.2 - 15 Oct 2015 35 | 36 | * Include bugfix by indraastra for CORS requests without preflights. 37 | -------------------------------------------------------------------------------- /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 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2014 Spark IO 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/spark/softap-setup-js.svg)](https://travis-ci.org/spark/softap-setup-js) 2 | [![Open Issues](https://img.shields.io/github/issues/spark/softap-setup-js.svg)](https://github.com/spark/softap-setup-js/issues) 3 | 4 | # softap-setup-js 5 | Particle SoftAP Setup for JavaScript — Perform wireless setup of Particle devices with Node.js or a browser. 6 | 7 | See also [softap-setup-ts, a community-created Typescript port of the SoftAP Setup library](https://github.com/markterrill/softap-setup-ts). 8 | 9 | ## Installation 10 | 11 | ``` 12 | npm install softap-setup 13 | ``` 14 | 15 | ## Configuration 16 | 17 | When instantiating a SoftAPSetup object, simply provide an options object with values for the keys you would like to override. 18 | 19 | ### Defaults 20 | 21 | ``` 22 | { 23 | "host": "192.168.0.1", 24 | "keepAlive": true, 25 | "timeout": 2000, 26 | "noDelay": true, 27 | "channel": 6, 28 | "protocol": "tcp", 29 | "port": 5609 (will default to 80 if protocol = http and no port specified) 30 | } 31 | ``` 32 | 33 | ## Usage 34 | 35 | ### Protocols 36 | 37 | This library supports both TCP and HTTP protocols for performing SoftAP setup. TCP is the default protocol. If you would like to use the HTTP protocol instead, simply override the `protocol` configuration parameter. 38 | 39 | #### Example: 40 | ```js 41 | var SoftAPSetup = require('softap-setup'); 42 | var sap = new SoftAPSetup({ protocol: 'http' }); 43 | 44 | sap.deviceInfo(callback); 45 | function callback(err, dat) { 46 | if (err) { throw err; } 47 | console.log("Device ID: %s, claimed: %s", dat.id, dat.claimed ? "yes" : "no"); 48 | }; 49 | ``` 50 | The above code will substitute the standard TCP port 5609 for HTTP on port 80. All methods function exactly the same regardless of chosen protocol. TCP is still the recommended protocol unless browser compatibility is specifically required. 51 | 52 | ### Obtaining Device Information 53 | 54 | You may often find it necessary to query the ID and claim status of your device. This is accomplished with a single method: `deviceInfo`. This method takes a single argument, which should be a reference to a callback function with the standard Node.js callback signature (example below). 55 | 56 | #### Example: 57 | ```js 58 | var SoftAPSetup = require('softap-setup'); 59 | var sap = new SoftAPSetup(); 60 | 61 | sap.deviceInfo(callback); 62 | function callback(err, dat) { 63 | if (err) { throw err; } 64 | console.log("Device ID: %s, claimed: %s", dat.id, dat.claimed ? "yes" : "no"); 65 | }; 66 | ``` 67 | The above code will attempt to retrieve the device information from the device (assuming the system running this code is connected to the device in SoftAP mode), with the default configuration (192.168.0.1:5609). When the underlying request is successfully fulfilled, the callback is called with no error parameter (null), and a result object containing the `id` of the device as a string of hex, as well as the `claimed` status as a boolean value, like so: 68 | 69 | ```json 70 | { "id": "STRINGOFHEXREPRESENTINGYOURDEVICEIDHERE", "claimed": false } 71 | ``` 72 | 73 | ### Obtaining the Device's Public Key 74 | 75 | The public key must be obtained from the device before it can be successfully configured. This is due to the fact that the public key is used to encrypt the passphrase for any AP that is configured to use security (see below). If you do not already know the public key of the device, you may request it from the device with the following command: 76 | 77 | #### Example 78 | ```js 79 | var sap = new SoftAPSetup(); 80 | sap.publicKey(callback); 81 | function callback(err, dat) { 82 | if (err) { throw err; } 83 | console.log(dat); 84 | }; 85 | ``` 86 | 87 | ### Setting the Device's Claim Code 88 | 89 | The device must be provided with a claim code before it can be registered to a user's account. This is a string that is typically provided by a call to the [Particle API](https://docs.particle.io/reference/api/#create-a-claim-code). Once such a claim code has been obtained, it can be provided to the device like so: 90 | 91 | #### Example 92 | ```js 93 | var code = "somekindaclaimcode"; 94 | var sap = newSoftAPSetup(); 95 | sap.setClaimCode(code, callback); 96 | function callback(err, dat) { 97 | if(err) { throw err; } 98 | console.log(dat); 99 | }; 100 | ``` 101 | 102 | ### Scanning for Nearby Access Points 103 | 104 | While connected to the device in SoftAP mode, it is possible to request a list of access points that have been detected in the area. This is done by way of the `scan` command. This command is one of the few commands that will typically take more than a few hundred milliseconds to complete. When executed, the device will listen for access points which are broadcasting their SSID. Important to note here is that it's not possible to detect networks which don't broadcast their SSID. You can still configure a non-broadcast network manually (see below). 105 | 106 | #### Example 107 | 108 | ```js 109 | var sap = new SoftAPSetup(); 110 | sap.scan(callback); 111 | function callback(err, dat) { 112 | if(err) { throw err; } 113 | console.log("Networks Identified:"); 114 | console.log(dat); 115 | }; 116 | ``` 117 | 118 | ### Configuring the Device for a Selected Access Point 119 | 120 | The following code snippet will store the provided details on the device, and cause it to attempt to connect to the AP you specify. If it is unsuccessful; it will return to SoftAP mode shortly thereafter, so you may reconnect to it and try again. 121 | 122 | #### Example 123 | ```js 124 | var sap = new SoftAPSetup(); 125 | sap.configure({ 126 | ssid: "", 127 | security: "", 128 | password: "[PASSWORD]", 129 | channel: "" 130 | }, callback); 131 | ``` 132 | 133 | #### Example: WPA Enterprise with PEAP/MSCHAPv2 authentication 134 | ```js 135 | var sap = new SoftAPSetup(); 136 | sap.configure({ 137 | ssid: "", 138 | security: "", 139 | username: "[USERNAME]", 140 | password: "[PASSWORD]", 141 | outer_identity: "[OUTER IDENTITY (optional)]", 142 | ca: "[CA CERTIFICATE (in PEM format, optional)]", 143 | channel: "" 144 | }, callback); 145 | ``` 146 | 147 | #### Example: WPA Enterprise with EAP-TLS authentication 148 | ```js 149 | var sap = new SoftAPSetup(); 150 | sap.configure({ 151 | ssid: "", 152 | security: "", 153 | client_certificate: "[CLIENT CERTIFICATE (in PEM format)]", 154 | private_key: "[PRIVATE KEY (in PEM format)]", 155 | outer_identity: "[OUTER IDENTITY (optional)]", 156 | ca: "[CA CERTIFICATE (in PEM format, optional)]", 157 | channel: "" 158 | }, callback); 159 | ``` 160 | 161 | ### Connecting to a Previously Configured Access Point 162 | 163 | Once you have successfully issued a `configure` command, it's now only a matter of giving the device the go-ahead to actually connect. As you may have guessed, this is done via the `connect` command. It takes only a callback parameter, and will always execute "successfully". Since there is no way to verify that the provided configuration is correct until a connection attempt is made; you will need to verify that the device is able to successfully connect to the cloud (most likely via an API request to the cloud to check for the presence of the device ID that was just configured). 164 | 165 | #### Example 166 | ```js 167 | var sap = new SoftAPSetup(); 168 | sap.connect(callback); 169 | function callback(err, dat) { 170 | console.log("Device is attempting to connect to the AP..."); 171 | }; 172 | ``` 173 | 174 | ### Wireless Security Types 175 | 176 | Valid security types are as follows: 177 | 178 | 1. "open" or "none" - no security 179 | 2. "wep_psk" - WEP pre-shared key 180 | 3. "wep_shared" - Open WEP 181 | 4. "wpa_tkip" — WPA with TKIP 182 | 5. "wpa_aes" — WPA with AES 183 | 6. "wpa2_tkip" - WPA2 with TKIP 184 | 7. "wpa2_aes" — WPA2 with AES 185 | 8. "wpa2_mixed" — WPA2 AES & TKIP 186 | 9. "wpa_enterprise_aes" — WPA Enterprise with AES 187 | 10. "wpa_enterprise_tkip" — WPA Enterprise with TKIP 188 | 11. "wpa2_enterprise_aes" — WPA2 Enterprise with AES 189 | 12. "wpa2_enterprise_tkip" — WPA2 Enterprise with TKIP 190 | 13. "wpa2_enterprise_mixed" — WPA2 Enterprise with AES/TKIP 191 | 14. "wpa2_enterprise" — WPA2 Enterprise with AES/TKIP 192 | 193 | ## Notes 194 | 195 | ### AP Password Security 196 | 197 | It's worth noting that this library uses the public key of the device to encrypt any AP passwords that are sent when configuring and connecting your device. 198 | 199 | ### EAP-TLS Private Key Security 200 | 201 | This library also uses the public key of the device and a random AES encryption key to encrypt the Private Key when configuring your device to connect to WPA Enterprise access point with EAP-TLS authentication. 202 | 203 | ## Running in the Browser 204 | 205 | It's possible to do SoftAP configuration from within a web browser. However, you must first convert the SoftAP code (and all of it's dependencies) from a Node.js module into a single javascript file. 206 | 207 | ### Browserify 208 | 209 | Install browserify: 210 | ```js 211 | npm install -g browserify 212 | ``` 213 | From the softap-setup-js code directory, run: 214 | ```js 215 | browserify index.js -s SoftAPSetup -o softap-browser.js 216 | ``` 217 | This will create a browser-friendly ```softap-browser.js``` file that exports the ```SoftAPSetup``` object. The only difference with the browser version of this object is that it does *NOT* support reading the configuration from a file. All other methods described above will work. 218 | 219 | **NOTE:** *Only the "http" protocol works in the browser. "tcp" will fail because the browser does not allow direct access to sockets.* 220 | 221 | #### Example: 222 | ```js 223 | 224 | 225 | 226 | 227 | 228 | 237 | 238 | 239 | ``` 240 | The above code will print the device info to the javascript console of the browser. 241 | -------------------------------------------------------------------------------- /example/basic-http-setup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SoftAPSetup = require('../index'); 4 | var config = require('./config'); 5 | var path = require('path'); 6 | 7 | var sap = new SoftAPSetup({ protocol: 'http', port: 80 }); 8 | 9 | if (!config.get('ssid')) { 10 | 11 | console.log('* Please specify the ssid of the AP with which you wish to connect your device...'); 12 | console.log('Example: %s %s --ssid BestWiFiNetworkEver --password SuperSecretPassword --security wpa2_mixed', process.argv[0], path.relative(process.cwd(), __filename)); 13 | process.exit(1); 14 | } 15 | 16 | console.log('Obtaining device information...'); 17 | 18 | sap.deviceInfo(claim); 19 | 20 | function claim(err, dat) { 21 | if (err) { 22 | console.error('Error getting claim code', err); 23 | return; 24 | } 25 | console.log(dat); 26 | console.log('-------'); 27 | console.log('Obtained device information. Setting claim code...'); 28 | sap.setClaimCode('wat', key); 29 | } 30 | 31 | function key(err, dat) { 32 | 33 | if (err) { 34 | throw err; 35 | } 36 | console.log(dat); 37 | console.log('-------'); 38 | console.log('Requesting public key...'); 39 | sap.publicKey(scan); 40 | } 41 | 42 | function scan(err, dat) { 43 | 44 | if (err) { 45 | throw err; 46 | } 47 | console.log(dat); 48 | console.log('-------'); 49 | console.log('Received public key. Scanning for APs...'); 50 | sap.scan(configure); 51 | } 52 | 53 | function configure(err, dat) { 54 | 55 | if (err) { 56 | throw err; 57 | } 58 | console.log(dat); 59 | console.log('-------'); 60 | console.log('Scanned APs. Configuring device...'); 61 | 62 | sap.configure({ 63 | ssid: config.get('ssid') 64 | , channel: config.get('channel') || 11 65 | , password: config.get('password') || undefined 66 | , security: config.get('security') || undefined 67 | }, connect); 68 | 69 | } 70 | 71 | function connect(err, dat) { 72 | 73 | if (err) { 74 | throw err; 75 | } 76 | console.log('Configured device. Issuing connect request...'); 77 | 78 | sap.connect(done); 79 | } 80 | 81 | function done(err, dat) { 82 | 83 | if (err) { 84 | throw err; 85 | } 86 | console.log('Successfully sent connect request. Now wait for breathing cyan!'); 87 | 88 | } 89 | -------------------------------------------------------------------------------- /example/basic-tcp-setup.js: -------------------------------------------------------------------------------- 1 | var SoftAPSetup = require('../index'); 2 | var config = require('./config'); 3 | var path = require('path'); 4 | 5 | var sap = new SoftAPSetup(); 6 | 7 | if(!config.get('ssid')) { 8 | 9 | console.log("* Please specify the ssid of the AP with which you wish to connect your device..."); 10 | console.log("Example: %s %s --ssid BestWiFiNetworkEver --password SuperSecretPassword --security wpa2_mixed", 11 | process.argv[0], path.relative(process.cwd(), __filename)); 12 | process.exit(1); 13 | } 14 | 15 | function deviceInfo() { 16 | console.log("Obtaining device information..."); 17 | sap.deviceInfo(claim); 18 | } 19 | 20 | deviceInfo(); 21 | 22 | function claim(err, dat) { 23 | if (err) { 24 | console.log('Error obtaining device information', err); 25 | deviceInfo(); 26 | return; 27 | } 28 | console.log(dat); 29 | console.log("-------"); 30 | console.log("Obtained device information. Setting claim code..."); 31 | sap.setClaimCode("wat", key); 32 | } 33 | 34 | function key(err, dat) { 35 | 36 | if (err) { throw err; } 37 | console.log(dat); 38 | console.log("-------"); 39 | console.log("Requesting public key..."); 40 | sap.publicKey(scan); 41 | } 42 | 43 | function scan(err, dat) { 44 | 45 | if(err) { throw err; } 46 | console.log(dat); 47 | console.log("-------"); 48 | console.log("Received public key. Scanning for APs..."); 49 | sap.scan(configure); 50 | } 51 | 52 | function configure(err, dat) { 53 | 54 | if(err) { throw err; } 55 | console.log(dat); 56 | console.log("-------"); 57 | console.log("Scanned APs. Configuring device..."); 58 | 59 | sap.configure({ 60 | ssid: config.get('ssid') 61 | , channel: config.get('channel') || 11 62 | , password: config.get('password') || undefined 63 | , security: config.get('security') || undefined 64 | }, connect); 65 | 66 | } 67 | 68 | function connect(err, dat) { 69 | 70 | if(err) { throw err; } 71 | console.log("Configured device. Issuing connect request..."); 72 | 73 | sap.connect(done); 74 | } 75 | 76 | function done(err, dat) { 77 | 78 | if(err) { throw err; } 79 | console.log("Successfully sent connect request. Now wait for breathing cyan!"); 80 | 81 | } 82 | -------------------------------------------------------------------------------- /example/config.js: -------------------------------------------------------------------------------- 1 | function Config() { 2 | return { 3 | 'ssid' : 'your ssid', 4 | "password" : "your p@$$vv0rD", 5 | "security" : "WPA2-ASK", 6 | 7 | get: function(key) { 8 | return this[key]; 9 | } 10 | } 11 | } 12 | 13 | module.exports = new Config(); 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var HttpSoftAP = require('./lib/HttpSoftAP'); 4 | var TcpSoftAP = require('./lib/TcpSoftAP'); 5 | var SoftAP = require('./lib/SoftAP.js'); 6 | 7 | var defaultPortMapping = { 8 | tcp: 5609, 9 | http: 80 10 | }; 11 | 12 | function SoftAPSetup(options) { 13 | var opts = SoftAP.defaultOptions(); 14 | opts.protocol = 'tcp'; 15 | 16 | SoftAP.assign(opts, options); 17 | 18 | if (!opts.port) { 19 | opts.port = defaultPortMapping[opts.protocol]; 20 | } 21 | 22 | if (opts.protocol === 'tcp') { 23 | return new TcpSoftAP(opts); 24 | } else if (opts.protocol === 'http') { 25 | return new HttpSoftAP(opts); 26 | } else { 27 | throw new Error('unknown protocol'); 28 | } 29 | } 30 | 31 | module.exports = SoftAPSetup; 32 | -------------------------------------------------------------------------------- /lib/HttpSoftAP.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SoftAP = require('./SoftAP'); 4 | var util = require('util'); 5 | var http = require('http'); 6 | 7 | function HttpSoftAP(options) { 8 | SoftAP.call(this, options); 9 | } 10 | util.inherits(HttpSoftAP, SoftAP); 11 | 12 | HttpSoftAP.prototype._sendProtocolCommand = function _sendProtocolCommand(cmd, cb) { 13 | var payload; 14 | 15 | if (!cmd || typeof cmd !== 'object') { 16 | throw new Error('Invalid command object specified.'); 17 | } 18 | 19 | var opts = { 20 | method: 'GET', 21 | path: '/' + cmd.name, 22 | hostname: this.host, 23 | port: this.port, 24 | protocol: 'http:' 25 | }; 26 | 27 | if ((cmd.body) && typeof cmd.body === 'object') { 28 | payload = JSON.stringify(cmd.body); 29 | // NOTE: 'Content-Type' is set here to make this a "simple" cross-site 30 | // request, as per the HTTP CORS docs: 31 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Simple_requests 32 | // According to the spec, this means that POST can be made directly 33 | // without an OPTIONS request being made first. 34 | opts.headers = { 35 | 'Content-Length': payload.length, 36 | 'Content-Type': 'application/x-www-form-urlencoded' 37 | }; 38 | opts.method = 'POST'; 39 | } 40 | 41 | var req = http.request(opts); 42 | req.setTimeout(this.timeout); 43 | 44 | var to = setTimeout(function socketTimedOut() { 45 | req.abort(); 46 | cb(new Error('HTTP timed out')); 47 | }, this.timeout); 48 | 49 | req.on('response', function responseHandler(res) { 50 | var results = ''; 51 | res.on('data', function dataHandler(chunk) { 52 | if (chunk) { 53 | results += chunk.toString(); 54 | } 55 | }); 56 | res.once('end', function httpEnd() { 57 | clearTimeout(to); 58 | 59 | var json; 60 | try { 61 | json = JSON.parse(results.toString()); 62 | } catch (e) { 63 | return cb(new Error('Invalid JSON received from device.')); 64 | } 65 | cb(null, json); 66 | }); 67 | }); 68 | 69 | req.once('error', function httpError(err) { 70 | clearTimeout(to); 71 | cb(err); 72 | }); 73 | 74 | if (payload) { 75 | req.write(payload); 76 | } 77 | req.end(); 78 | }; 79 | 80 | module.exports = HttpSoftAP; 81 | -------------------------------------------------------------------------------- /lib/SoftAP.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var RSA = require('node-rsa'); 4 | var crypto = require('crypto'); 5 | 6 | var securityTable = { 7 | open: 0, 8 | none: 0, 9 | wep_psk: 1, 10 | wep_shared: 0x8001, 11 | wpa_tkip: 0x00200002, 12 | wpa_aes: 0x00200004, 13 | wpa2_aes: 0x00400004, 14 | wpa2_tkip: 0x00400002, 15 | wpa2_mixed: 0x00400006, 16 | wpa2: 0x00400006, 17 | wpa_enterprise_aes: 0x02200004, 18 | wpa_enterprise_tkip: 0x02200002, 19 | wpa2_enterprise_aes: 0x02400004, 20 | wpa2_enterprise_tkip: 0x02400002, 21 | wpa2_enterprise_mixed: 0x02400006, 22 | wpa2_enterprise: 0x02400006, 23 | enterprise: 0x02000000 24 | }; 25 | 26 | var eapTypeTable = { 27 | peap: 25, 28 | 'peap/mschapv2': 25, 29 | 'eap-tls': 13, 30 | tls: 13 31 | }; 32 | 33 | function is(cb) { 34 | if (cb && typeof cb == 'function') { 35 | return true; 36 | } 37 | throw new Error('Invalid callback function provided.'); 38 | } 39 | 40 | function formatPem(data) { 41 | return data.trim() + '\r\n'; 42 | }; 43 | 44 | function SoftAP(options) { 45 | var opts = SoftAP.defaultOptions(); 46 | SoftAP.assign(opts, options); 47 | 48 | this.keepAlive = opts.keepAlive; 49 | this.noDelay = opts.noDelay; 50 | this.timeout = opts.timeout; 51 | this.host = opts.host; 52 | this.port = opts.port; 53 | 54 | this.__publicKey = undefined; 55 | 56 | return this; 57 | } 58 | 59 | SoftAP.defaultOptions = function defaultOptions() { 60 | var opts = { 61 | host: '192.168.0.1', 62 | keepAlive: true, 63 | timeout: 8000, 64 | noDelay: true, 65 | channel: 6 66 | }; 67 | 68 | return opts; 69 | }; 70 | 71 | SoftAP.assign = function assign(opts, options) { 72 | if (options && typeof options == 'object') { 73 | Object.keys(options).forEach(function _loadOpts(key) { 74 | opts[key] = options[key]; 75 | }); 76 | } 77 | }; 78 | 79 | function checkResponse(err, dat, cb) { 80 | if (err) { 81 | return cb(err); 82 | } 83 | if (!dat) { 84 | return cb(new Error('No data received')); 85 | } 86 | if (dat.r !== 0) { 87 | return cb(new Error('Received non-zero response code')); 88 | } 89 | } 90 | 91 | SoftAP.prototype.scan = function scan(cb) { 92 | is(cb); 93 | this.__sendCommand({ name: 'scan-ap' }, function response(err, json) { 94 | if (err) { 95 | return cb(err); 96 | } 97 | cb(null, json.scans); 98 | }); 99 | }; 100 | 101 | SoftAP.prototype.connect = function connect(index, cb) { 102 | if (!cb) { 103 | cb = index; 104 | index = 0; 105 | } 106 | is(cb); 107 | this.__sendCommand({ name: 'connect-ap', body: { idx: index } }, function response(err, dat) { 108 | checkResponse(err, dat, cb); 109 | cb(); 110 | }); 111 | }; 112 | 113 | SoftAP.prototype.deviceInfo = function deviceInfo(cb) { 114 | is(cb); 115 | this.__sendCommand({ name: 'device-id' }, function response(err, dat) { 116 | if (err) { 117 | return cb(err); 118 | } 119 | 120 | var claimed = dat.c === '1'; 121 | var id = dat.id && dat.id.toLowerCase(); 122 | this.__deviceID = id; 123 | 124 | cb(null, { 125 | id : id, 126 | claimed : claimed 127 | }); 128 | }.bind(this)); 129 | }; 130 | 131 | SoftAP.prototype.publicKey = function publicKey(cb) { 132 | is(cb); 133 | this.__sendCommand({ name: 'public-key' }, function response(err, dat) { 134 | checkResponse(err, dat, cb); 135 | var buff = new Buffer(dat.b, 'hex'); 136 | this.__publicKey = new RSA(buff.slice(22), 'pkcs1-public-der', { 137 | encryptionScheme: 'pkcs1' 138 | }); 139 | cb(null, this.__publicKey.exportKey('pkcs8-public')); 140 | }.bind(this)); 141 | }; 142 | 143 | SoftAP.prototype.setClaimCode = function setClaimCode(code, cb) { 144 | is(cb); 145 | if (!code || typeof code !== 'string') { 146 | throw new Error('Must provide claim code string as first parameter'); 147 | } 148 | var claim = { 149 | k: 'cc', 150 | v: code 151 | }; 152 | this.set(claim, cb); 153 | }; 154 | 155 | SoftAP.prototype.set = function set(data, cb) { 156 | is(cb); 157 | this.__sendCommand({ name: 'set', body: data }, function response(err, dat) { 158 | checkResponse(err, dat, cb); 159 | cb(); 160 | }); 161 | }; 162 | 163 | SoftAP.prototype.aesEncrypt = function(data, kiv) { 164 | if (!kiv) { 165 | kiv = crypto.randomBytes(32); 166 | } 167 | var kivEncrypted = this.__publicKey.encrypt(kiv, 'hex'); 168 | var cipher = crypto.createCipheriv('aes-128-cbc', kiv.slice(0, 16), kiv.slice(16, 32)); 169 | var encrypted = cipher.update(data, 'utf8', 'hex'); 170 | encrypted += cipher.final('hex'); 171 | 172 | return { 173 | kiv: kivEncrypted, 174 | encrypted: encrypted 175 | }; 176 | }; 177 | 178 | SoftAP.prototype.configure = function configure(opts, cb) { 179 | is(cb); 180 | 181 | var securePass = undefined; 182 | 183 | if (!this.__publicKey) { 184 | throw new Error('Must retrieve public key of device prior to AP configuration'); 185 | } 186 | if (!opts || typeof opts !== 'object') { 187 | throw new Error('Missing configuration options object as first parameter'); 188 | } 189 | if (!opts.ssid) { 190 | if (!opts.name) { 191 | throw new Error('Configuration options contain no ssid property'); 192 | } 193 | opts.ssid = opts.name; 194 | } 195 | 196 | if ((opts.enc || opts.sec) && !opts.security) { 197 | opts.security = opts.sec || opts.enc; 198 | } 199 | if (!opts.security) { 200 | opts.security = 'open'; 201 | opts.password = null; 202 | } 203 | if (opts.password || opts.pass) { 204 | if (!opts.security) { 205 | throw new Error('Password provided but no security type specified'); 206 | } 207 | if (opts.pass && !opts.password) { 208 | opts.password = opts.pass; 209 | } 210 | securePass = this.__publicKey.encrypt(opts.password, 'hex'); 211 | } 212 | if (typeof opts.security === 'string') { 213 | opts.security = securityTable[opts.security]; 214 | } 215 | 216 | var apConfig = { 217 | idx: opts.index || 0, 218 | ssid: opts.ssid, 219 | sec: opts.security, 220 | ch: parseInt(opts.channel) 221 | }; 222 | 223 | if (opts.security & securityTable.enterprise) { 224 | if (!opts.eap) 225 | throw new Error('Security is set to Enterprise, but no EAP type provided'); 226 | if (typeof opts.eap === 'string') { 227 | opts.eap = eapTypeTable[opts.eap.toLowerCase()]; 228 | } 229 | if (opts.eap === undefined) { 230 | throw new Error('Unknown EAP type provided'); 231 | } 232 | if (opts.eap == eapTypeTable.peap) { 233 | // inner identity and password are mandatory 234 | opts.inner_identity = opts.inner_identity || opts.username; 235 | if (!opts.inner_identity || !opts.password) { 236 | throw new Error('PEAP credentials missing'); 237 | } 238 | apConfig.ii = opts.inner_identity; 239 | // Password is set later on 240 | } else if (opts.eap == eapTypeTable.tls) { 241 | // client certificate and private key are mandatory 242 | if (!opts.private_key || !opts.client_certificate) { 243 | throw new Error('EAP-TLS credentials missing'); 244 | } 245 | apConfig.crt = formatPem(opts.client_certificate); 246 | var enc = this.aesEncrypt(formatPem(opts.private_key)); 247 | apConfig.key = enc.encrypted; 248 | apConfig.ek = enc.kiv; 249 | } 250 | apConfig.eap = opts.eap; 251 | if (opts.outer_identity) { 252 | apConfig.oi = opts.outer_identity; 253 | } 254 | opts.ca = opts.ca || opts.root_ca; 255 | if (opts.ca) { 256 | apConfig.ca = formatPem(opts.ca); 257 | } 258 | } 259 | 260 | if (securePass) { 261 | apConfig.pwd = securePass; 262 | } 263 | 264 | 265 | this.__sendCommand({ name: 'configure-ap', body: apConfig }, cb); 266 | }; 267 | 268 | SoftAP.prototype.__sendCommand = function __sendCommand(cmd, cb) { 269 | is(cb); 270 | if (typeof cmd == 'object') { 271 | if (!cmd.name) { 272 | throw new Error('Command object has no name property'); 273 | } 274 | } else { 275 | throw new Error('Invalid command'); 276 | } 277 | 278 | return this._sendProtocolCommand(cmd, cb); 279 | }; 280 | 281 | SoftAP.prototype._sendProtocolCommand = function _sendProtocolCommand() { 282 | throw new Error('Implemented in protocol specific class'); 283 | }; 284 | 285 | SoftAP.prototype.version = function version(cb) { 286 | is(cb); 287 | this.__sendCommand({ name: 'version' }, cb); 288 | }; 289 | 290 | SoftAP.prototype.securityValue = function(name) { 291 | return securityTable[name.toLowerCase()]; 292 | }; 293 | 294 | SoftAP.prototype.securityLookup = function securityLookup(dec) { 295 | var match = null; 296 | Object.keys(securityTable).forEach(function securityType(key) { 297 | if (parseInt(dec) === securityTable[key]) { 298 | match = key; 299 | } 300 | }); 301 | return match; 302 | }; 303 | 304 | SoftAP.prototype.eapTypeValue = function(name) { 305 | return eapTypeTable[name.toLowerCase()]; 306 | }; 307 | 308 | module.exports = SoftAP; 309 | -------------------------------------------------------------------------------- /lib/TcpSoftAP.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SoftAP = require('./SoftAP'); 4 | var util = require('util'); 5 | var net = require('net'); 6 | 7 | function TcpSoftAP(options) { 8 | SoftAP.call(this, options); 9 | } 10 | util.inherits(TcpSoftAP, SoftAP); 11 | 12 | TcpSoftAP.prototype._sendProtocolCommand = function _sendProtocolCommand(cmd, cb) { 13 | var that = this; 14 | 15 | function sendRealCommand() { 16 | sendCommand.bind(that)(cmd, cb); 17 | } 18 | 19 | /** 20 | * The first command sent to the device seems to keep the socket open, so we send a no-op 21 | * command and throw it away. 22 | */ 23 | if (!this.warmedUp) { 24 | this.warmedUp = true; 25 | sendCommand.bind(that)({name: 'device-id'}, sendRealCommand, true, 2000); 26 | } 27 | else { 28 | sendRealCommand(); 29 | } 30 | 31 | function sendCommand(cmd, cb, forceClose, timeoutOverride) { 32 | var err, json; 33 | var data = ''; 34 | 35 | var sock = net.createConnection(this.port, this.host); 36 | sock.setNoDelay(this.noDelay); 37 | sock.setTimeout(timeoutOverride || this.timeout); 38 | sock.setKeepAlive(this.keepAlive); 39 | 40 | var to = setTimeout(function socketTimedOut() { 41 | err = new Error('TCP socket timed out'); 42 | sock.destroy(); 43 | }, this.timeout); 44 | 45 | sock.once('connect', function socketConnected() { 46 | var send; 47 | if (cmd.body && typeof cmd.body === 'object') { 48 | var body = JSON.stringify(cmd.body); 49 | var length = body.length; 50 | send = util.format('%s\n%s\n\n%s', cmd.name, length, body); 51 | } else { 52 | send = util.format('%s\n0\n\n', cmd.name); 53 | } 54 | 55 | sock.write(send); 56 | }); 57 | 58 | sock.on('data', function socketData(chunk) { 59 | data += chunk.toString(); 60 | try { 61 | json = JSON.parse(data); 62 | clearTimeout(to); 63 | if (forceClose) { 64 | sock.end(); 65 | } 66 | } catch (e) { 67 | // Wait for more data to come in 68 | } 69 | }); 70 | 71 | sock.once('error', function socketError(error) { 72 | err = error; 73 | clearTimeout(to); 74 | }); 75 | 76 | sock.once('timeout', function socketTimeout() { 77 | clearTimeout(to); 78 | err = new Error('TCP socket timed out'); 79 | sock.destroy(); 80 | }); 81 | 82 | sock.once('close', function socketClose(hadError) { 83 | if (!err && hadError) { 84 | err = new Error('unknown socket error'); 85 | } 86 | cb(err, json); 87 | }); 88 | } 89 | }; 90 | 91 | module.exports = TcpSoftAP; 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "softap-setup", 3 | "version": "4.1.0", 4 | "description": "Node Client for SoftAP Setup", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=4" 8 | }, 9 | "browser": "./lib/HttpSoftAP.js", 10 | "scripts": { 11 | "test": "mocha test/", 12 | "coveralls": "istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec test/ && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage", 13 | "lint": "eslint -f unix **/*.js", 14 | "lint:fix": "eslint --fix -f unix **/*.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/spark/softap-setup-js.git" 19 | }, 20 | "author": { 21 | "name": "Emily Rose", 22 | "email": "nexxy@particle.io" 23 | }, 24 | "contributors": [ 25 | { 26 | "name": "Tom Distler", 27 | "email": "tom@brewsolid.com" 28 | }, 29 | { 30 | "name": "Mark Solters", 31 | "email": "msolters@gmail.com" 32 | }, 33 | { 34 | "name": "Vishal Talwar", 35 | "email": "rawlatv@gmail.com" 36 | } 37 | ], 38 | "license": "LGPL-3.0", 39 | "bugs": { 40 | "url": "https://github.com/spark/softap-setup-js/issues" 41 | }, 42 | "homepage": "https://github.com/spark/softap-setup-js", 43 | "dependencies": { 44 | "node-rsa": "^0.4.2" 45 | }, 46 | "devDependencies": { 47 | "chai": "^3.5.0", 48 | "coveralls": "^2.11.4", 49 | "eslint": "^3.15.0", 50 | "eslint-config-particle": "^1.0.2", 51 | "istanbul": "^0.4.0", 52 | "mocha": "^3.2.0", 53 | "should": "^8.2.2", 54 | "sinon": "^1.17.7", 55 | "sinon-chai": "^2.8.0", 56 | "softap-emulator-js": "^1.0.3" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "rules": { 6 | "func-names": 0 7 | } 8 | } -------------------------------------------------------------------------------- /test/SoftAP.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('should'); 4 | 5 | var SoftAP = require('../lib/SoftAP.js'); 6 | var sinon = require('sinon'); 7 | var chai = require('chai'); 8 | var sinonChai = require('sinon-chai'); 9 | chai.use(sinonChai); 10 | var expect = chai.expect; 11 | 12 | 13 | describe('softap', function() { 14 | 15 | describe('constructor', function() { 16 | it('can be constructed with options undefined', function() { 17 | var sut = new SoftAP(); 18 | var defaults = SoftAP.defaultOptions(); 19 | sut.should.have.property('host').be.eql(defaults.host); 20 | sut.should.have.property('keepAlive').be.eql(defaults.keepAlive); 21 | }); 22 | 23 | it('can be constructed with options overridden', function() { 24 | var sut = new SoftAP({ host:'abcd' }); 25 | var defaults = SoftAP.defaultOptions(); 26 | sut.should.have.property('host').be.eql('abcd'); 27 | sut.should.have.property('keepAlive').be.eql(defaults.keepAlive); 28 | }); 29 | }); 30 | 31 | describe('deviceInfo', function() { 32 | it('converts the deviceID to lowercase', function() { 33 | var sut = new SoftAP(); 34 | sut.__sendCommand = sinon.spy(function(command, cb) { 35 | cb(undefined, { id:'ABCD', c:'1' }); 36 | }); 37 | 38 | var cb = sinon.stub(); 39 | sut.deviceInfo(cb); 40 | expect(cb).to.have.been.calledWith(null, { id:'abcd', claimed:true }); 41 | }); 42 | }); 43 | 44 | }); 45 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('should'); 4 | var SoftAPSetup = require('../index'); 5 | var SoftAPEmulator = require('softap-emulator-js'); 6 | var assert = require('assert'); 7 | // var domain = require('domain'); 8 | var RSA = require('node-rsa'); 9 | var net = require('net'); 10 | 11 | // function noop() { }; 12 | var TEST_KEY = '30818902818100a633b9fdee23da72b0d40c4669eaf8101f0157cb971d8d16a5f1a91379a0d59b48acc887b9d15dd103225a583461abfe8c6008bb03d74d58c3d50fecd89244cf4c42269808fe5646a2eaca7a8ee14ba0d1921bd4e3ebd47d7c9552b07fc93ad31d4543b927956b170a4c53f34886f45c48dcfbce18003cc99370bf61def099e90203010001'; 13 | 14 | var testConfig = { host: '127.0.0.1', port: 5609, timeout: 3000 }; 15 | var server; 16 | 17 | describe('SoftAPSetup', function () { 18 | before(function (done) { 19 | var emu = new SoftAPEmulator(); 20 | server = net.createServer(emu.server()); 21 | server.listen( 22 | testConfig.port, 23 | testConfig.host, function () { 24 | done(); 25 | }); 26 | }); 27 | 28 | describe('#deviceInfo', function() { 29 | it('Successfully retrieves device info', function(done) { 30 | var sap = new SoftAPSetup(testConfig); 31 | sap.deviceInfo(function(err, dat) { 32 | if (err) { 33 | return done(err); 34 | } 35 | try { 36 | dat.should.have.property('id').be.a.String; 37 | dat.should.have.property('claimed').be.Boolean; 38 | done(); 39 | } catch (e) { 40 | done(e); 41 | } 42 | }); 43 | }); 44 | 45 | it('Throws an error when given an invalid callback', function() { 46 | try { 47 | var sap = new SoftAPSetup(testConfig); 48 | sap.deviceInfo('callback'); 49 | } catch (e) { 50 | assert.equal('Invalid callback function provided.', e.message); 51 | } 52 | }); 53 | }); 54 | 55 | describe('#scan', function() { 56 | it('Successfully retrieves AP list', function (done) { 57 | var sap = new SoftAPSetup(testConfig); 58 | sap.scan(function(err, dat) { 59 | if (err) { 60 | return done(err); 61 | } 62 | try { 63 | dat.should.be.Array; 64 | dat.should.matchAny(function(scan) { 65 | scan.should.have.property('ssid').be.a.String; 66 | scan.should.have.property('sec').be.a.Number; 67 | }); 68 | done(); 69 | } catch (e) { 70 | done(e); 71 | } 72 | }); 73 | }); 74 | }); 75 | 76 | describe('#publicKey', function() { 77 | it('Successfully retrieves what looks like a public key', function (done) { 78 | var sap = new SoftAPSetup(testConfig); 79 | sap.publicKey(function(err, key) { 80 | if (err) { 81 | return done(err); 82 | } 83 | try { 84 | key.should.be.a.String; 85 | key.should.startWith('-----BEGIN PUBLIC KEY-----\n'); 86 | done(); 87 | } catch (e) { 88 | done(e); 89 | } 90 | }); 91 | }); 92 | }); 93 | 94 | describe('#setClaimCode', function() { 95 | it('Successfully sets a claim code', function (done) { 96 | var sap = new SoftAPSetup(testConfig); 97 | sap.setClaimCode('asdfasdf', done); 98 | }); 99 | }); 100 | 101 | describe('#configure', function() { 102 | var conf = { 103 | ssid : 'hi', 104 | security: 'wpa2', 105 | password: 'hi' 106 | }; 107 | 108 | var confEnterprise = { 109 | ssid: 'ent', 110 | security: 'wpa2_enterprise' 111 | } 112 | 113 | it('Throws an error when called before publicKey is obtained', function (done) { 114 | var sap = new SoftAPSetup({ host: '127.0.0.1', port: 5609 }); 115 | try { 116 | sap.configure(conf, function cb() { 117 | done(new Error('Configure did not throw an error')); 118 | }); 119 | } catch (e) { 120 | assert.equal('Must retrieve public key of device prior to AP configuration', e.message); 121 | done(); 122 | } 123 | }); 124 | 125 | it('Successfully sends configuration details', function (done) { 126 | var sap = new SoftAPSetup({ host: '127.0.0.1', port: 5609 }); 127 | 128 | sap.__publicKey = new RSA(new Buffer(TEST_KEY, 'hex'), 'pkcs1-public-der', { 129 | encryptionScheme: 'pkcs1' 130 | }); 131 | 132 | sap.configure(conf, done); 133 | }); 134 | 135 | it('Throws an error when security is set to Enterprise but no EAP type or wrong EAP type is provided', function(done) { 136 | var sap = new SoftAPSetup({ host: '127.0.0.1', port: 5609 }); 137 | 138 | sap.__publicKey = new RSA(new Buffer(TEST_KEY, 'hex'), 'pkcs1-public-der', { 139 | encryptionScheme: 'pkcs1' 140 | }); 141 | 142 | try { 143 | sap.configure(confEnterprise, function cb() { 144 | done(new Error('Configure did not throw an error')); 145 | }); 146 | } catch (e) { 147 | assert.equal('Security is set to Enterprise, but no EAP type provided', e.message); 148 | } 149 | 150 | try { 151 | var c = Object.assign({}, confEnterprise); 152 | c.eap = 'leap'; 153 | sap.configure(c, function cb() { 154 | done(new Error('Configure did not throw an error')); 155 | }); 156 | } catch (e) { 157 | assert.equal('Unknown EAP type provided', e.message); 158 | } 159 | 160 | done(); 161 | }); 162 | 163 | it('Throws an error when EAP is set to PEAP and no PEAP credentials are provided', function(done) { 164 | var sap = new SoftAPSetup({ host: '127.0.0.1', port: 5609 }); 165 | 166 | sap.__publicKey = new RSA(new Buffer(TEST_KEY, 'hex'), 'pkcs1-public-der', { 167 | encryptionScheme: 'pkcs1' 168 | }); 169 | 170 | try { 171 | var c = Object.assign({}, confEnterprise); 172 | c.eap = 'peap'; 173 | sap.configure(c, function cb() { 174 | done(new Error('Configure did not throw an error')); 175 | }); 176 | } catch (e) { 177 | assert.equal('PEAP credentials missing', e.message); 178 | } 179 | 180 | done(); 181 | }); 182 | 183 | it('Throws an error when EAP is set to EAP-TLS and no EAP-TLS credentials are provided', function(done) { 184 | var sap = new SoftAPSetup({ host: '127.0.0.1', port: 5609 }); 185 | 186 | sap.__publicKey = new RSA(new Buffer(TEST_KEY, 'hex'), 'pkcs1-public-der', { 187 | encryptionScheme: 'pkcs1' 188 | }); 189 | 190 | try { 191 | var c = Object.assign({}, confEnterprise); 192 | c.eap = 'eap-tls'; 193 | sap.configure(c, function cb() { 194 | done(new Error('Configure did not throw an error')); 195 | }); 196 | } catch (e) { 197 | assert.equal('EAP-TLS credentials missing', e.message); 198 | } 199 | 200 | done(); 201 | }); 202 | 203 | it('Successfully sends PEAP configuration details', function(done){ 204 | var sap = new SoftAPSetup({ host: '127.0.0.1', port: 5609 }); 205 | 206 | sap.__publicKey = new RSA(new Buffer(TEST_KEY, 'hex'), 'pkcs1-public-der', { 207 | encryptionScheme: 'pkcs1' 208 | }); 209 | 210 | var c = Object.assign({}, confEnterprise); 211 | c.eap = 'peap'; 212 | c.username = 'username'; 213 | c.password = 'password'; 214 | c.outer_identity = 'anonymous'; 215 | c.ca = 'TESTCACERTIFICATE'; 216 | 217 | sap.configure(c, done); 218 | }); 219 | 220 | it('Successfully sends EAP-TLS configuration details', function(done){ 221 | var sap = new SoftAPSetup({ host: '127.0.0.1', port: 5609 }); 222 | 223 | sap.__publicKey = new RSA(new Buffer(TEST_KEY, 'hex'), 'pkcs1-public-der', { 224 | encryptionScheme: 'pkcs1' 225 | }); 226 | 227 | var c = Object.assign({}, confEnterprise); 228 | c.eap = 'eap-tls'; 229 | c.private_key = 'TESTPRIVATEKEY'; 230 | c.client_certificate = 'TESTCLIENTCERTIFICATE'; 231 | c.outer_identity = 'anonymous'; 232 | c.ca = 'TESTCACERTIFICATE'; 233 | 234 | sap.configure(c, done); 235 | }); 236 | }); 237 | 238 | describe('#connect', function() { 239 | it('Successfully sends command to connect', function (done) { 240 | var sap = new SoftAPSetup({ host: '127.0.0.1', port: 5609 }); 241 | sap.connect(done); 242 | }); 243 | }); 244 | }); 245 | --------------------------------------------------------------------------------