homebridge-nest-cam2
Advanced tools
Comparing version 0.0.26 to 0.0.27
77
index.js
@@ -6,3 +6,7 @@ 'use strict'; | ||
const NestConnection = require('./lib/nest-connection.js'); | ||
const Promise = require('bluebird'); | ||
Promise.delay = function(time_ms) { | ||
return new Promise(resolve => setTimeout(resolve, time_ms)); | ||
} | ||
const modelTypes = { | ||
@@ -26,23 +30,20 @@ 8: 'Nest Cam Indoor', | ||
const setupConnection = function(config, log) { | ||
return new Promise(function (resolve, reject) { | ||
if (!config.access_token && !config.googleAuth && (!config.email || !config.password)) { | ||
reject('You did not specify your Nest account credentials {\'email\',\'password\'}, or an access_token, or googleAuth, in config.json'); | ||
return; | ||
} | ||
if (config.googleAuth && (!config.googleAuth.issueToken || !config.googleAuth.cookies || !config.googleAuth.apiKey)) { | ||
reject('When using googleAuth, you must provide issueToken, cookies and apiKey in config.json. Please see README.md for instructions'); | ||
const setupConnection = async function(config, log) { | ||
if (!config.googleAuth) { | ||
reject('You did not specify your Google account credentials, googleAuth, in config.json'); | ||
return; | ||
} | ||
} | ||
const conn = new NestConnection(config, log); | ||
conn.auth().then(connected => { | ||
if (connected) { | ||
resolve(conn); | ||
} else { | ||
reject('Unable to connect to Nest service.'); | ||
} | ||
}); | ||
}); | ||
if (config.googleAuth && (!config.googleAuth.issueToken || !config.googleAuth.cookies || !config.googleAuth.apiKey)) { | ||
reject('You must provide issueToken, cookies and apiKey in config.json. Please see README.md for instructions'); | ||
return; | ||
} | ||
const conn = new NestConnection(config, log); | ||
try { | ||
let connected = await conn.auth(); | ||
return connected; | ||
} catch(error) { | ||
throw('Unable to connect to Nest service.', error); | ||
} | ||
}; | ||
@@ -73,5 +74,14 @@ | ||
*/ | ||
addCameras(accessToken) { | ||
async addCameras(accessToken) { | ||
let self = this; | ||
self.nestAPI = new Nest(accessToken, self.config, self.log); | ||
// Nest needs to be reauthenticated about every hour | ||
setInterval(async function() { | ||
let connected = await setupConnection(self.config, self.log); | ||
if (connected) { | ||
self.nestAPI.accessToken = self.config.access_token; | ||
} | ||
}, 3600000); | ||
self.nestAPI.on('cameras', (cameras) => { | ||
@@ -101,4 +111,4 @@ let configuredAccessories = []; | ||
.getCharacteristic(Characteristic.On) | ||
.on('set', function(value, callback) { | ||
camera.toggleActive(value); | ||
.on('set', async function(value, callback) { | ||
await camera.toggleActive(value); | ||
self.log.info("Setting %s to %s", accessory.displayName, (value ? 'on' : 'off')); | ||
@@ -112,6 +122,6 @@ callback(); | ||
}); | ||
self.nestAPI.fetchCameras(); | ||
await self.nestAPI.fetchCameras(); | ||
} | ||
didFinishLaunching() { | ||
async didFinishLaunching() { | ||
let self = this; | ||
@@ -123,16 +133,7 @@ let googleAuth = self.config['googleAuth']; | ||
} | ||
setupConnection(self.config, self.log) | ||
.then(function(conn){ | ||
return; | ||
}) | ||
.then(function(data) { | ||
self.addCameras(self.config.access_token); | ||
}) | ||
.catch(function(err) { | ||
self.log.error(err); | ||
if (callback) { | ||
callback([]); | ||
} | ||
}); | ||
let connected = await setupConnection(self.config, self.log); | ||
if (connected) { | ||
await self.addCameras(self.config.access_token); | ||
} | ||
} | ||
} |
@@ -6,4 +6,3 @@ /** | ||
const Promise = require('bluebird'); | ||
const rp = require('request-promise'); | ||
const axios = require('axios'); | ||
@@ -28,97 +27,65 @@ 'use strict'; | ||
function Connection(config, log) { | ||
this.config = config; | ||
this.log = log; | ||
this.token = ''; | ||
this.config = config; | ||
this.log = log; | ||
} | ||
Connection.prototype.auth = function() { | ||
return new Promise(resolve => { | ||
Promise.coroutine(function* () { | ||
let req, body; | ||
Connection.prototype.auth = async function() { | ||
let req, body; | ||
this.connected = false; | ||
this.token = null; | ||
//Only doing google auth from now on | ||
let issueToken = this.config.googleAuth.issueToken; | ||
let cookies = this.config.googleAuth.cookies; | ||
//Only doing google auth from now on | ||
let issueToken = this.config.googleAuth.issueToken; | ||
let cookies = this.config.googleAuth.cookies; | ||
this.log.debug('Authenticating via Google.'); | ||
req = { | ||
method: 'GET', | ||
followAllRedirects: true, | ||
timeout: API_TIMEOUT_SECONDS * 1000, | ||
uri: issueToken, | ||
headers: { | ||
'Sec-Fetch-Mode': 'cors', | ||
'User-Agent': USER_AGENT_STRING, | ||
'X-Requested-With': 'XmlHttpRequest', | ||
'Referer': 'https://accounts.google.com/o/oauth2/iframe', | ||
'cookie': cookies | ||
}, | ||
json: true | ||
}; | ||
let result = yield rp(req); | ||
let googleAccessToken = result.access_token; | ||
req = { | ||
method: 'POST', | ||
followAllRedirects: true, | ||
timeout: API_TIMEOUT_SECONDS * 1000, | ||
uri: 'https://nestauthproxyservice-pa.googleapis.com/v1/issue_jwt', | ||
body: { | ||
embed_google_oauth_access_token: true, | ||
expire_after: '3600s', | ||
google_oauth_access_token: googleAccessToken, | ||
policy_id: 'authproxy-oauth-policy' | ||
}, | ||
headers: { | ||
'Authorization': 'Bearer ' + googleAccessToken, | ||
'User-Agent': USER_AGENT_STRING, | ||
'x-goog-api-key': this.config.googleAuth.apiKey, | ||
'Referer': 'https://home.nest.com' | ||
}, | ||
json: true | ||
}; | ||
result = yield rp(req); | ||
this.config.access_token = result.jwt; | ||
if (this.config.access_token && this.config.googleAuth) { | ||
req = { | ||
method: 'GET', | ||
followAllRedirects: true, | ||
timeout: API_TIMEOUT_SECONDS * 1000, | ||
uri: URL_NEST_AUTH, | ||
headers: { | ||
'Authorization': 'Basic ' + this.config.access_token, | ||
'User-Agent': USER_AGENT_STRING | ||
}, | ||
json: true | ||
}; | ||
} else { | ||
resolve(false); | ||
return; | ||
} | ||
try { | ||
body = yield rp(req); | ||
this.connected = true; | ||
this.token = body.access_token; | ||
this.transport_url = body.urls.transport_url; | ||
this.userid = body.userid; | ||
resolve(true); | ||
} catch(error) { | ||
this.connected = false; | ||
if (error.statusCode == 400) { | ||
this.log.error('Auth failed: access token specified in Homebridge configuration rejected'); | ||
resolve(false); | ||
} else if (error.statusCode == 429) { | ||
this.log.error('Auth failed: rate limit exceeded. Please try again in 60 minutes'); | ||
resolve(false); | ||
} else { | ||
this.log.error('Could not authenticate with Nest (code ' + (error.statusCode || (error.cause && error.cause.code)) + '). Retrying in ' + API_AUTH_FAIL_RETRY_DELAY_SECONDS + ' second(s).'); | ||
Promise.delay(API_AUTH_FAIL_RETRY_DELAY_SECONDS * 1000).then(() => this.auth()).then(connected => resolve(connected)); | ||
} | ||
} | ||
}).call(this); | ||
}); | ||
this.log.debug('Authenticating via Google.'); | ||
let result; | ||
try { | ||
req = { | ||
method: 'GET', | ||
timeout: API_TIMEOUT_SECONDS * 1000, | ||
url: issueToken, | ||
headers: { | ||
'Sec-Fetch-Mode': 'cors', | ||
'User-Agent': USER_AGENT_STRING, | ||
'X-Requested-With': 'XmlHttpRequest', | ||
'Referer': 'https://accounts.google.com/o/oauth2/iframe', | ||
'cookie': cookies | ||
} | ||
}; | ||
result = (await axios(req)).data; | ||
let googleAccessToken = result.access_token; | ||
if (result.error) { | ||
this.log.error('Google authentication was unsuccessful. Make sure you did not log out of your Google account after getting your googleAuth parameters.'); | ||
throw(result); | ||
} | ||
req = { | ||
method: 'POST', | ||
timeout: API_TIMEOUT_SECONDS * 1000, | ||
url: 'https://nestauthproxyservice-pa.googleapis.com/v1/issue_jwt', | ||
data: { | ||
embed_google_oauth_access_token: true, | ||
expire_after: '3600s', | ||
google_oauth_access_token: googleAccessToken, | ||
policy_id: 'authproxy-oauth-policy' | ||
}, | ||
headers: { | ||
'Authorization': 'Bearer ' + googleAccessToken, | ||
'User-Agent': USER_AGENT_STRING, | ||
'x-goog-api-key': this.config.googleAuth.apiKey, | ||
'Referer': 'https://home.nest.com' | ||
} | ||
}; | ||
result = (await axios(req)).data; | ||
this.config.access_token = result.jwt; | ||
return true; | ||
} catch(error) { | ||
error.status = error.response && error.response.status; | ||
this.log.error('Access token acquisition via googleAuth failed (code ' + (error.status || error.code) + ').'); | ||
if (['ECONNREFUSED','ESOCKETTIMEDOUT','ECONNABORTED','ENOTFOUND','ENETUNREACH'].includes(error.code)) { | ||
this.log.error('Retrying in ' + API_AUTH_FAIL_RETRY_DELAY_SECONDS + ' second(s).'); | ||
await Promise.delay(API_AUTH_FAIL_RETRY_DELAY_SECONDS * 1000); | ||
return await this.auth(); | ||
} else { | ||
return false; | ||
} | ||
} | ||
}; |
139
lib/nest.js
'use strict'; | ||
const https = require('https'); | ||
const querystring = require('querystring'); | ||
const axios = require('axios'); | ||
const EventEmitter = require('events'); | ||
const NestCam = require('./nestcam').NestCam; | ||
const NestConnection = require('./nest-connection.js'); | ||
const NestAPIHostname = 'https://webapi.camera.home.nest.com'; | ||
const NestAPIHostname = 'webapi.camera.home.nest.com'; | ||
const NestAuthAPIHostname = 'home.nest.com'; | ||
class NestAPI extends EventEmitter { | ||
@@ -18,76 +15,25 @@ constructor(accessToken, config, log) { | ||
self.log = log; | ||
// Nest needs to be reauthenticated about every hour | ||
const interval = setInterval(() => { | ||
self.reauth(config, log) | ||
.then(function(conn){ | ||
return; | ||
}) | ||
.then(function(data) { | ||
self.accessToken = config.access_token; | ||
}) | ||
.catch(function(err) { | ||
self.log.error(err); | ||
if (callback) { | ||
callback([]); | ||
} | ||
}); | ||
}, 3600000); | ||
} | ||
/** | ||
* Reauthenticate the google access user_token | ||
* @param config The configuration object | ||
* @param log The logger | ||
*/ | ||
reauth(config, log) { | ||
return new Promise(function (resolve, reject) { | ||
let self = this; | ||
const conn = new NestConnection(config, log); | ||
conn.auth().then(connected => { | ||
if (connected) { | ||
resolve(conn); | ||
} else { | ||
reject('Unable to connect to Nest service.'); | ||
} | ||
}); | ||
}); | ||
}; | ||
/** | ||
* Fetch cameras from nest and add them to Homebridge | ||
*/ | ||
fetchCameras() { | ||
async fetchCameras() { | ||
let self = this; | ||
self.sendHomeRequest('/api/cameras.get_owned_and_member_of_with_properties', 'GET') | ||
.then((response) => { | ||
let text = response.toString(); | ||
let json = JSON.parse(text); | ||
if (json.status === 0) { | ||
var cameras = []; | ||
json.items.forEach((cameraInfo) => { | ||
let camera = new NestCam(self, cameraInfo, self.log); | ||
cameras.push(camera); | ||
}); | ||
self.emit('cameras', cameras); | ||
} else { | ||
self.log.error('Failed to load cameras. ' + json.status_detail); | ||
} | ||
}) | ||
.catch((err) => { | ||
self.log.error('Failed to load cameras. ' + err.message); | ||
}); | ||
let response = await self.sendRequest(NestAPIHostname, '/api/cameras.get_owned_and_member_of_with_properties', 'GET'); | ||
if (response) { | ||
if (response.status === 0) { | ||
var cameras = []; | ||
response.items.forEach((cameraInfo) => { | ||
let camera = new NestCam(self, cameraInfo, self.log); | ||
cameras.push(camera); | ||
}); | ||
self.emit('cameras', cameras); | ||
} else { | ||
self.log.error('Failed to load cameras. ' + response.status_detail); | ||
} | ||
} | ||
} | ||
/** | ||
* Send api request to the camera endpoint | ||
* @param endpoint The endpoint to send the request | ||
* @param method Usually "GET" or "POST" | ||
* @param body The body of the request or null if a "GET" | ||
*/ | ||
sendHomeRequest(endpoint, method, body) { | ||
let self = this; | ||
return self.sendRequest(NestAPIHostname, endpoint, method, body); | ||
} | ||
/** | ||
* Send a generic api request | ||
@@ -99,42 +45,29 @@ * @param hostname The base uri to send the request | ||
*/ | ||
sendRequest(hostname, endpoint, method, body) { | ||
async sendRequest(hostname, endpoint, method, body) { | ||
let self = this; | ||
let headers = { | ||
'User-Agent': 'iPhone iPhone OS 11.0 Dropcam/5.14.0 com.nestlabs.jasper.release Darwin', | ||
'Referer': 'https://home.nest.com/' | ||
}; | ||
return new Promise((resolve, reject) => { | ||
let headers = { | ||
'User-Agent': 'iPhone iPhone OS 11.0 Dropcam/5.14.0 com.nestlabs.jasper.release Darwin', | ||
'Referer': 'https://home.nest.com/' | ||
}; | ||
if (method === 'POST') { | ||
headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8'; | ||
} | ||
if (method === 'POST') { | ||
headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8'; | ||
} | ||
if (self.accessToken !== undefined) { | ||
headers['Cookie'] = 'user_token=' + self.accessToken; | ||
} | ||
if (self.accessToken !== undefined) { | ||
headers['Cookie'] = 'user_token=' + self.accessToken; | ||
} | ||
let options = { | ||
hostname: hostname, | ||
path: endpoint, | ||
try { | ||
let req = { | ||
method: method, | ||
url: hostname + endpoint, | ||
data: body, | ||
headers: headers | ||
}; | ||
let req = https.request(options, (res) => { | ||
if (res.statusCode < 200 || res.statusCode >= 300) { | ||
let error = new Error('Unexpected API Error - ' + res.statusCode); | ||
error.code = res.statusCode; | ||
reject(error); | ||
} | ||
const resBody = []; | ||
res.on('data', (chunk) => resBody.push(chunk)); | ||
res.on('end', () => resolve(Buffer.concat(resBody))); | ||
}); | ||
req.on('error', (err) => reject(err)); | ||
if (body !== undefined) { | ||
req.write(body); | ||
} | ||
req.end(); | ||
}); | ||
return (await axios(req)).data; | ||
} catch(error) { | ||
error.status = error.response && error.response.status; | ||
self.log.error('Unexpected API Error - ' + (error.status || error.code)); | ||
} | ||
} | ||
@@ -141,0 +74,0 @@ } |
@@ -5,4 +5,2 @@ 'use strict'; | ||
const fs = require('fs'); | ||
const ip = require('ip'); | ||
const spawn = require('child_process').spawn; | ||
@@ -12,2 +10,3 @@ const querystring = require('querystring'); | ||
const ModelTypes = require('./protos/ModelTypes.js').ModelTypes; | ||
const NestAPIHostname = 'https://webapi.camera.home.nest.com'; | ||
@@ -28,6 +27,6 @@ class NestCam { | ||
self.nexusTalkHost = info.direct_nexustalk_host; | ||
self.apiHost = info.nexus_api_http_server.slice(8); // remove https:// | ||
self.apiHost = info.nexus_api_http_server; | ||
} | ||
toggleActive(enabled) { | ||
async toggleActive(enabled) { | ||
let self = this; | ||
@@ -38,9 +37,4 @@ let query = querystring.stringify({ | ||
}); | ||
self.api.sendHomeRequest('/api/dropcams.set_properties', 'POST', query) | ||
.then((response) => { | ||
self.enabled = enabled; | ||
}) | ||
.catch((err) => { | ||
self.log.error(err); | ||
}); | ||
let response = await self.api.sendRequest(NestAPIHostname, '/api/dropcams.set_properties', 'POST', query); | ||
self.enabled = enabled; | ||
} | ||
@@ -84,6 +78,3 @@ | ||
let self = this; | ||
// This is for backward compatibility with the old useOMX config value | ||
if (config.useOMX) { | ||
self.ffmpegCodec = "h264_omx"; | ||
} else if (config.ffmpegCodec) { | ||
if (config.ffmpegCodec) { | ||
self.ffmpegCodec = config.ffmpegCodec; | ||
@@ -156,3 +147,3 @@ } | ||
handleSnapshotRequest(request, callback) { | ||
async handleSnapshotRequest(request, callback) { | ||
let self = this; | ||
@@ -163,9 +154,4 @@ let query = querystring.stringify({ | ||
}); | ||
self.api.sendRequest(self.apiHost, '/get_image?' + query, 'GET') | ||
.then((response) => { | ||
callback(undefined, response); | ||
}) | ||
.catch((err) => { | ||
callback(err); | ||
}); | ||
let response = await self.api.sendRequest(self.apiHost, '/get_image?' + query, 'GET') | ||
callback(undefined, response); | ||
} | ||
@@ -172,0 +158,0 @@ |
{ | ||
"name": "homebridge-nest-cam2", | ||
"version": "0.0.26", | ||
"version": "0.0.27", | ||
"description": "Nest cam plugin for homebridge: https://homebridge.io/", | ||
@@ -23,6 +23,4 @@ "license": "ISC", | ||
"pbf": "^3.1.0", | ||
"bluebird": "^3.5.4", | ||
"request": "^2.88.0", | ||
"request-promise": "^4.2.4" | ||
"axios": "^0.19.2" | ||
} | ||
} |
@@ -5,2 +5,6 @@ # homebridge-nest-cam2 | ||
[![NPM](https://nodei.co/npm/homebridge-nest-cam2.png?compact=true)](https://nodei.co/npm/homebridge-nest-cam2/) | ||
[![Beerpay](https://beerpay.io/Brandawg93/homebridge-nest-cam2/badge.svg)](https://beerpay.io/Brandawg93/homebridge-nest-cam2) | ||
![npm](https://img.shields.io/npm/dt/homebridge-nest-cam2) | ||
## Notes | ||
@@ -10,12 +14,2 @@ - This is a continuation of the previous [homebridge-nest-cam](https://github.com/KhaosT/homebridge-nest-cam) plugin. | ||
## Changelog | ||
| Date | Version | Description | | ||
|---------|---------|-------------------------------| | ||
| 1/24/20 | 0.20.0 | Initial Commit | | ||
| 1/24/20 | 0.22.0 | Zero Latency Tune | | ||
| 1/25/20 | 0.23.0 | Fix Google Reauthentication | | ||
| 1/26/20 | 0.24.0 | Nest Hello Resolution Support | | ||
| 1/28/20 | 0.25.0 | Toggle Streaming | | ||
| 2/05/20 | 0.26.0 | Performance Improvements | | ||
## FAQ | ||
@@ -22,0 +16,0 @@ Q: Why is there no audio? |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
3
1
3
57500
1117
77
+ Addedaxios@^0.19.2
+ Addedaxios@0.19.2(transitive)
+ Addedfollow-redirects@1.5.10(transitive)
- Removedbluebird@^3.5.4
- Removedrequest@^2.88.0
- Removedrequest-promise@^4.2.4
- Removedajv@6.12.6(transitive)
- Removedasn1@0.2.6(transitive)
- Removedassert-plus@1.0.0(transitive)
- Removedasynckit@0.4.0(transitive)
- Removedaws-sign2@0.7.0(transitive)
- Removedaws4@1.13.0(transitive)
- Removedbcrypt-pbkdf@1.0.2(transitive)
- Removedbluebird@3.7.2(transitive)
- Removedcaseless@0.12.0(transitive)
- Removedcombined-stream@1.0.8(transitive)
- Removedcore-util-is@1.0.2(transitive)
- Removeddashdash@1.14.1(transitive)
- Removeddelayed-stream@1.0.0(transitive)
- Removedecc-jsbn@0.1.2(transitive)
- Removedextend@3.0.2(transitive)
- Removedextsprintf@1.3.0(transitive)
- Removedfast-deep-equal@3.1.3(transitive)
- Removedfast-json-stable-stringify@2.1.0(transitive)
- Removedforever-agent@0.6.1(transitive)
- Removedform-data@2.3.3(transitive)
- Removedgetpass@0.1.7(transitive)
- Removedhar-schema@2.0.0(transitive)
- Removedhar-validator@5.1.5(transitive)
- Removedhttp-signature@1.2.0(transitive)
- Removedis-typedarray@1.0.0(transitive)
- Removedisstream@0.1.2(transitive)
- Removedjsbn@0.1.1(transitive)
- Removedjson-schema@0.4.0(transitive)
- Removedjson-schema-traverse@0.4.1(transitive)
- Removedjson-stringify-safe@5.0.1(transitive)
- Removedjsprim@1.4.2(transitive)
- Removedlodash@4.17.21(transitive)
- Removedmime-db@1.52.0(transitive)
- Removedmime-types@2.1.35(transitive)
- Removedoauth-sign@0.9.0(transitive)
- Removedperformance-now@2.1.0(transitive)
- Removedpsl@1.9.0(transitive)
- Removedpunycode@2.3.1(transitive)
- Removedqs@6.5.3(transitive)
- Removedrequest@2.88.2(transitive)
- Removedrequest-promise@4.2.6(transitive)
- Removedrequest-promise-core@1.1.4(transitive)
- Removedsafe-buffer@5.2.1(transitive)
- Removedsafer-buffer@2.1.2(transitive)
- Removedsshpk@1.18.0(transitive)
- Removedstealthy-require@1.1.1(transitive)
- Removedtough-cookie@2.5.0(transitive)
- Removedtunnel-agent@0.6.0(transitive)
- Removedtweetnacl@0.14.5(transitive)
- Removeduri-js@4.4.1(transitive)
- Removeduuid@3.4.0(transitive)
- Removedverror@1.10.0(transitive)