Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb229897f7 | ||
|
|
3927b7e383 | ||
|
|
afae1e3ada | ||
|
|
f4cf2696f8 | ||
|
|
365234d49a | ||
|
|
0d6f9265f7 | ||
|
|
75ddfc38b1 | ||
|
|
06508d1797 | ||
|
|
521907fa7d | ||
|
|
2f276c7ce2 | ||
|
|
f10056ec2e | ||
|
|
779cb1b7a2 | ||
|
|
bbe4def5cf | ||
|
|
88a50a3387 | ||
|
|
3b2a09a1da | ||
|
|
c99496527d | ||
|
|
32b2d721e5 | ||
|
|
945323a7da | ||
|
|
8b400a45b5 | ||
|
|
3adee03533 | ||
|
|
4288fd5980 | ||
|
|
ec57131d51 | ||
|
|
6bfe590b0d | ||
|
|
f7c1857b6a | ||
|
|
6a9c62b639 | ||
|
|
ed873cdd4a | ||
|
|
14bf276112 | ||
|
|
93452d445f | ||
|
|
a436f17888 | ||
|
|
947b4d7118 | ||
|
|
b152071085 | ||
|
|
9dcd55a84f | ||
|
|
342138d92e | ||
|
|
ef712de9e3 | ||
|
|
b2c5f5ecc9 | ||
|
|
f0b9c32ab4 | ||
|
|
3fc13317a2 | ||
|
|
73295729c8 | ||
|
|
ed3239e4db | ||
|
|
f2d64d7676 | ||
|
|
1a83dcc1de | ||
|
|
4b17c3c9c3 | ||
|
|
9cb623c096 | ||
|
|
7306f7a8a5 | ||
|
|
4eb1c61c4b | ||
|
|
306193e6e9 | ||
|
|
0ca44d7ffb | ||
|
|
3012f29bb2 | ||
|
|
8d588507ac | ||
|
|
7e6eebc486 | ||
|
|
4be709a61b | ||
|
|
d74c56f182 | ||
|
|
444af51307 | ||
|
|
bde0bfe616 | ||
|
|
b13b9a54ab | ||
|
|
68ea4887b1 | ||
|
|
75b10bb528 | ||
|
|
f444ff753c | ||
|
|
9dd6980da2 | ||
|
|
ec1360217b | ||
|
|
8743cebb01 | ||
|
|
ae4526defe | ||
|
|
2cba450128 | ||
|
|
b9fd78390f | ||
|
|
9787dd2be7 | ||
|
|
defa0c2d41 | ||
|
|
23da93538e | ||
|
|
63b7a27021 | ||
|
|
a2f70a8e70 | ||
|
|
480ae5efd6 | ||
|
|
a735f0d9e7 | ||
|
|
eb812e9754 | ||
|
|
330c6fc720 | ||
|
|
3ebc249ffa | ||
|
|
b38b3cf5ab | ||
|
|
b1ed1f4f2f | ||
|
|
8f5effe84c | ||
|
|
4d255acfc6 | ||
|
|
7fdd04343b | ||
|
|
30221f1d4e | ||
|
|
729a4dc6ae | ||
|
|
87007e20c6 | ||
|
|
549f4f0323 | ||
|
|
3f72bdd84b | ||
|
|
ef8aeda02d | ||
|
|
222f8cd701 | ||
|
|
624cf01270 | ||
|
|
c6328ce2f4 | ||
|
|
5ab49014b0 | ||
|
|
c43933225b | ||
|
|
4ef216679d | ||
|
|
ca2cce8ab6 | ||
|
|
c58f93aeb2 | ||
|
|
7e7a2032bb | ||
|
|
0d9ca3486b | ||
|
|
dc5c12c8ce | ||
|
|
09b3d7cb1c | ||
|
|
6338f9f07c | ||
|
|
84e79bda9e | ||
|
|
720dbd05d2 | ||
|
|
00aacd212b | ||
|
|
843d183045 | ||
|
|
1cf455540a | ||
|
|
29d6f5b9d0 | ||
|
|
f639435a09 | ||
|
|
9db29bb55d | ||
|
|
48f861a6a5 | ||
|
|
1a5fd65f4e | ||
|
|
475e3fffbe | ||
|
|
3a178a5744 | ||
|
|
ceeb213899 | ||
|
|
38db068c29 |
No files matched your search
@@ -2,7 +2,6 @@
|
|||||||
lib/*.map
|
lib/*.map
|
||||||
lib/api-functions.js
|
lib/api-functions.js
|
||||||
lib/auth.js
|
lib/auth.js
|
||||||
lib/cache.js
|
|
||||||
lib/common.js
|
lib/common.js
|
||||||
lib/config.js
|
lib/config.js
|
||||||
lib/cwmp.js
|
lib/cwmp.js
|
||||||
|
|||||||
@@ -1,5 +1,53 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
## 1.1.2 (2018-02-24)
|
||||||
|
|
||||||
|
- A large number of bug fixes as well as stability and performance improvements.
|
||||||
|
- Three security vulnerabilities disclosed by Maximilian Hils have been patched.
|
||||||
|
- New config option UDP_CONNECTION_REQUEST_PORT to specify binding port for UDP
|
||||||
|
connection requests.
|
||||||
|
- New config option DATETIME_MILLISECONDS to strip milliseconds from dateTime
|
||||||
|
values.
|
||||||
|
- New config option BOOLEAN_LITERAL to use 1/0 or true/false for boolean values.
|
||||||
|
- Parameter values that cannot be parsed according to the reported type now show
|
||||||
|
a warning message.
|
||||||
|
- Virtual parameter scripts now use the variable 'args' instead of the special
|
||||||
|
'TIMESTAMPS' and 'VALUES' variables. The content of the args array is: {declare
|
||||||
|
timestamps}, {declare values}, {current timestamps}, {current values}.
|
||||||
|
- Virtual parameter value types are now inferred from the JavaScript type if the
|
||||||
|
returned value attribute is not a value-type pair.
|
||||||
|
- Show a fault when a virtual parameter script doesn't return the required
|
||||||
|
attributes.
|
||||||
|
- Redis is now optional (and disabled by default), reducing the complexity of
|
||||||
|
scalable deployments.
|
||||||
|
- Better detection of cyclical presets resulting in fewer faults for complex
|
||||||
|
provisioning scripts.
|
||||||
|
- Math.random() is now deterministic on per-device basis. A function has been
|
||||||
|
added to allow specifying a seed value (e.g. Math.random.seed(Date.now())).
|
||||||
|
- Overload spikes are now handled gracefully by refusing to accept new sessions
|
||||||
|
temporarily when under abnormal load.
|
||||||
|
- Added log messages for session timeouts, connection drops, and XML parsing
|
||||||
|
errors.
|
||||||
|
- Date.now() now takes an optional argument to specify "time steps" (in
|
||||||
|
milliseconds). This can be used to ensure a group of parameters are all
|
||||||
|
refreshed at the same time intervals.
|
||||||
|
- Only the non-default configuration options are now logged at process start.
|
||||||
|
- Faults caused by errors from extensions now show a cleaner stack trace.
|
||||||
|
- Exit main process if there are too many worker crashes (e.g. when DB is down).
|
||||||
|
- Updated dependencies and included a lockfile to ensure installations get the
|
||||||
|
exact dependencies it was tested against.
|
||||||
|
|
||||||
|
## 1.1.1 (2017-03-23)
|
||||||
|
|
||||||
|
- Avoid crashing when connection request credentials are missing.
|
||||||
|
- Show a warning instead of crashing when failing to parse parameter values
|
||||||
|
according to the expected value type.
|
||||||
|
- Add missing "Registered" event.
|
||||||
|
- Fix bug where in certain cases many more instances than declared are created.
|
||||||
|
- Fix parameter discovery bug when declared path timestamp is 1 or is not set.
|
||||||
|
- Fix preset precondition failing when testing against datetime parameters and
|
||||||
|
certain other parameters like \_deviceId.\_ProductClass.
|
||||||
|
|
||||||
## 1.1.0 (2017-03-10)
|
## 1.1.0 (2017-03-10)
|
||||||
|
|
||||||
- Provisions enable implementing dynamic device configuration or complex device
|
- Provisions enable implementing dynamic device configuration or complex device
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
GenieACS is a fast and lightweight TR-069 auto configuration server (ACS). This
|
GenieACS is a fast and lightweight TR-069 auto configuration server (ACS). This
|
||||||
is the core back end component. A GUI front end is available at
|
is the core back end component. A GUI front end is available at
|
||||||
https://github.com/zaidka/genieacs-gui.
|
https://github.com/genieacs/genieacs-gui.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -16,26 +16,31 @@ systems
|
|||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
Install [Node.js](http://nodejs.org/), [Redis](http://redis.io/), and
|
Install [Node.js](http://nodejs.org/) and [MongoDB](http://www.mongodb.org/).
|
||||||
[MongoDB](http://www.mongodb.org/). Refer to the corresponding documentation for
|
Refer to their corresponding documentation for installation instructions. The
|
||||||
installation guides. Supported versions are:
|
supported versions are:
|
||||||
|
|
||||||
- Node.js: 6.x and 7.x (7.x recommended)
|
- Node.js: 6.x and 8.x (8.x recommended)
|
||||||
- Redis: 2.4 through 3.2
|
|
||||||
- MongoDB: 2.6 through 3.4
|
- MongoDB: 2.6 through 3.4
|
||||||
|
|
||||||
|
Install build tools and libxml2 development files from your system's package
|
||||||
|
manager.
|
||||||
|
|
||||||
Then install GenieACS using NPM:
|
Then install GenieACS using NPM:
|
||||||
|
|
||||||
npm install -g genieacs
|
npm install -g genieacs
|
||||||
|
|
||||||
Alternatively, you can install from source by cloning the git repository:
|
Alternatively, you can install from source by cloning the git repository:
|
||||||
|
|
||||||
git clone https://github.com/zaidka/genieacs.git
|
git clone https://github.com/genieacs/genieacs.git
|
||||||
cd genieacs
|
cd genieacs
|
||||||
git checkout $(git tag -l v1.1.* --sort=-v:refname | head -n 1)
|
git checkout $(git tag -l v1.1.* --sort=-v:refname | head -n 1)
|
||||||
npm install
|
npm install
|
||||||
npm run compile
|
npm run compile
|
||||||
|
|
||||||
|
Before proceeding, find and review the file "config.json" in "config" directory
|
||||||
|
where GenieACS is downloaded.
|
||||||
|
|
||||||
Finally, run the following (from bin directory if installing from source):
|
Finally, run the following (from bin directory if installing from source):
|
||||||
|
|
||||||
genieacs-cwmp
|
genieacs-cwmp
|
||||||
@@ -57,13 +62,13 @@ such.
|
|||||||
Note: For production deployment make sure to run those as background services.
|
Note: For production deployment make sure to run those as background services.
|
||||||
|
|
||||||
For further details about installation and configuration, refer to the
|
For further details about installation and configuration, refer to the
|
||||||
[wiki section](https://github.com/zaidka/genieacs/wiki).
|
[wiki section](https://github.com/genieacs/genieacs/wiki).
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
The [Users mailing list](http://lists.genieacs.com) is a good place to get
|
The [Users mailing list](http://lists.genieacs.com) is a good place to get
|
||||||
guidance and help from the community. Head on over and join the conversation!
|
guidance and help from the community. Head on over and join the conversation!
|
||||||
In addition, the [wiki](https://github.com/zaidka/genieacs/wiki) provides useful
|
In addition, the [wiki](https://github.com/genieacs/genieacs/wiki) provides useful
|
||||||
documentation and tips from GenieACS users.
|
documentation and tips from GenieACS users.
|
||||||
|
|
||||||
For commercial support options and professional services, please visit
|
For commercial support options and professional services, please visit
|
||||||
@@ -71,5 +76,5 @@ For commercial support options and professional services, please visit
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright 2013-2017 Zaid Abdulla. GenieACS is released under the
|
Copyright 2013-2018 GenieACS Inc. GenieACS is released under the
|
||||||
[AGPLv3 license terms](https://raw.githubusercontent.com/zaidka/genieacs/master/LICENSE).
|
[AGPLv3 license terms](https://raw.githubusercontent.com/genieacs/genieacs/master/LICENSE).
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
{
|
{
|
||||||
"MONGODB_CONNECTION_URL" : "mongodb://127.0.0.1/genieacs",
|
"MONGODB_CONNECTION_URL" : "mongodb://127.0.0.1/genieacs",
|
||||||
"REDIS_PORT" : "6379",
|
|
||||||
"REDIS_HOST" : "127.0.0.1",
|
|
||||||
"CWMP_INTERFACE" : "0.0.0.0",
|
"CWMP_INTERFACE" : "0.0.0.0",
|
||||||
"CWMP_PORT" : 7547,
|
"CWMP_PORT" : 7547,
|
||||||
"CWMP_SSL" : false,
|
"CWMP_SSL" : false,
|
||||||
@@ -9,7 +7,6 @@
|
|||||||
"NBI_PORT" : 7557,
|
"NBI_PORT" : 7557,
|
||||||
"FS_INTERFACE" : "0.0.0.0",
|
"FS_INTERFACE" : "0.0.0.0",
|
||||||
"FS_PORT" : 7567,
|
"FS_PORT" : 7567,
|
||||||
"FS_IP" : "192.168.0.1",
|
"FS_HOSTNAME" : "acs.example.com",
|
||||||
"LOG_INFORMS" : true,
|
|
||||||
"DEBUG" : false
|
"DEBUG" : false
|
||||||
}
|
}
|
||||||
+23
-12
@@ -27,6 +27,7 @@ common = require './common'
|
|||||||
util = require 'util'
|
util = require 'util'
|
||||||
URL = require 'url'
|
URL = require 'url'
|
||||||
auth = require './auth'
|
auth = require './auth'
|
||||||
|
cache = require './cache'
|
||||||
|
|
||||||
|
|
||||||
udpConReq = (address, un, key, callback) ->
|
udpConReq = (address, un, key, callback) ->
|
||||||
@@ -46,7 +47,13 @@ udpConReq = (address, un, key, callback) ->
|
|||||||
|
|
||||||
message = Buffer.from("GET #{uri} HTTP/1.1\r\nHost: #{address}\r\n\r\n")
|
message = Buffer.from("GET #{uri} HTTP/1.1\r\nHost: #{address}\r\n\r\n")
|
||||||
|
|
||||||
client = dgram.createSocket('udp4')
|
client = dgram.createSocket({type: 'udp4', reuseAddr: true})
|
||||||
|
UDP_CONNECTION_REQUEST_PORT = config.get('UDP_CONNECTION_REQUEST_PORT')
|
||||||
|
if UDP_CONNECTION_REQUEST_PORT
|
||||||
|
# When a device is NAT'ed, the UDP Connection Request must originate from the same address and port used by the STUN server, in order to traverse the firewall.
|
||||||
|
# This does require that the Genieacs NBI and STUN server are allowed to bind to the same address and port.
|
||||||
|
# The STUN server needs to open its UDP port with the SO_REUSEADDR option, allowing the NBI to also bind to the same port.
|
||||||
|
client.bind({port: UDP_CONNECTION_REQUEST_PORT, exclusive: true})
|
||||||
|
|
||||||
count = 3
|
count = 3
|
||||||
client.send(message, 0, message.length, port, host, f = (err) ->
|
client.send(message, 0, message.length, port, host, f = (err) ->
|
||||||
@@ -58,8 +65,12 @@ udpConReq = (address, un, key, callback) ->
|
|||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
httpConReq = (url, username, password, timeout, callback) ->
|
httpConReq = (url, username, password, allowBasicAuth, timeout, callback) ->
|
||||||
options = URL.parse(url)
|
options = URL.parse(url)
|
||||||
|
|
||||||
|
if options.protocol != 'http:'
|
||||||
|
return callback(new Error('Invalid connection request URL or protocol'))
|
||||||
|
|
||||||
# Ensure socket is reused in case of digest authentication
|
# Ensure socket is reused in case of digest authentication
|
||||||
options.agent = new http.Agent({maxSockets : 1})
|
options.agent = new http.Agent({maxSockets : 1})
|
||||||
|
|
||||||
@@ -78,10 +89,14 @@ httpConReq = (url, username, password, timeout, callback) ->
|
|||||||
if res.statusCode == 401 and res.headers['www-authenticate']?
|
if res.statusCode == 401 and res.headers['www-authenticate']?
|
||||||
authHeader = auth.parseAuthHeader(res.headers['www-authenticate'])
|
authHeader = auth.parseAuthHeader(res.headers['www-authenticate'])
|
||||||
if authHeader.method is 'Basic'
|
if authHeader.method is 'Basic'
|
||||||
options.headers = {'Authorization' : auth.basic(username, password)}
|
if not allowBasicAuth
|
||||||
|
request.abort()
|
||||||
|
callback(new Error('Basic HTTP authentication not allowed'))
|
||||||
|
return
|
||||||
|
options.headers = {'Authorization' : auth.basic(username or '', password or '')}
|
||||||
else if authHeader.method is 'Digest'
|
else if authHeader.method is 'Digest'
|
||||||
options.headers = {
|
options.headers = {
|
||||||
'Authorization' : auth.digest(username, password, options.path, 'GET', null, authHeader)
|
'Authorization' : auth.digest(username or '', password or '', options.path, 'GET', null, authHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
request = http.get(options, (res) ->
|
request = http.get(options, (res) ->
|
||||||
@@ -123,6 +138,7 @@ httpConReq = (url, username, password, timeout, callback) ->
|
|||||||
|
|
||||||
connectionRequest = (deviceId, callback) ->
|
connectionRequest = (deviceId, callback) ->
|
||||||
CONNECTION_REQUEST_TIMEOUT = config.get('CONNECTION_REQUEST_TIMEOUT', deviceId)
|
CONNECTION_REQUEST_TIMEOUT = config.get('CONNECTION_REQUEST_TIMEOUT', deviceId)
|
||||||
|
CONNECTION_REQUEST_ALLOW_BASIC_AUTH = config.get('CONNECTION_REQUEST_ALLOW_BASIC_AUTH', deviceId)
|
||||||
proj = {
|
proj = {
|
||||||
'Device.ManagementServer.ConnectionRequestURL._value' : 1,
|
'Device.ManagementServer.ConnectionRequestURL._value' : 1,
|
||||||
'Device.ManagementServer.UDPConnectionRequestAddress._value' : 1,
|
'Device.ManagementServer.UDPConnectionRequestAddress._value' : 1,
|
||||||
@@ -151,7 +167,7 @@ connectionRequest = (deviceId, callback) ->
|
|||||||
conReq = () ->
|
conReq = () ->
|
||||||
udpSent = udpConReq(udpConnectionRequestAddress, username, password, (err) -> throw err if err)
|
udpSent = udpConReq(udpConnectionRequestAddress, username, password, (err) -> throw err if err)
|
||||||
|
|
||||||
httpConReq(connectionRequestUrl, username, password, CONNECTION_REQUEST_TIMEOUT, (err) ->
|
httpConReq(connectionRequestUrl, username, password, CONNECTION_REQUEST_ALLOW_BASIC_AUTH, CONNECTION_REQUEST_TIMEOUT, (err) ->
|
||||||
if udpSent
|
if udpSent
|
||||||
return callback()
|
return callback()
|
||||||
callback(err)
|
callback(err)
|
||||||
@@ -208,7 +224,7 @@ insertTasks = (tasks, callback) ->
|
|||||||
if tasks? and common.typeOf(tasks) isnt common.ARRAY_TYPE
|
if tasks? and common.typeOf(tasks) isnt common.ARRAY_TYPE
|
||||||
tasks = [tasks]
|
tasks = [tasks]
|
||||||
else if not tasks? or tasks.length == 0
|
else if not tasks? or tasks.length == 0
|
||||||
return callback(tasks)
|
return callback(null, tasks or [])
|
||||||
|
|
||||||
counter = tasks.length
|
counter = tasks.length
|
||||||
|
|
||||||
@@ -236,12 +252,7 @@ deleteDevice = (deviceId, callback) ->
|
|||||||
return callback(err) if err
|
return callback(err) if err
|
||||||
db.operationsCollection.remove({'_id' : {'$regex' : "^#{common.escapeRegExp(deviceId)}\\:"}}, (err) ->
|
db.operationsCollection.remove({'_id' : {'$regex' : "^#{common.escapeRegExp(deviceId)}\\:"}}, (err) ->
|
||||||
return callback(err) if err
|
return callback(err) if err
|
||||||
db.redisClient.del("#{deviceId}_presets_hash",
|
cache.del("#{deviceId}_tasks_faults_operations", callback)
|
||||||
"#{deviceId}_inform_hash",
|
|
||||||
"#{deviceId}_faults",
|
|
||||||
"#{deviceId}_tasks",
|
|
||||||
"#{deviceId}_operations",
|
|
||||||
callback)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
+24
-4
@@ -21,10 +21,30 @@ crypto = require 'crypto'
|
|||||||
|
|
||||||
|
|
||||||
exports.parseAuthHeader = (authHeader) ->
|
exports.parseAuthHeader = (authHeader) ->
|
||||||
res = {method: authHeader.slice(0, authHeader.indexOf(' '))}
|
authHeader = authHeader.trim()
|
||||||
re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi
|
method = authHeader.split(' ', 1)[0]
|
||||||
while match = re.exec(authHeader)
|
res = {method: method}
|
||||||
res[match[1]] = match[2] or match[3]
|
parts = authHeader.slice(method.length + 1).split(',')
|
||||||
|
|
||||||
|
while (part = parts.shift())?
|
||||||
|
name = part.split('=', 1)[0]
|
||||||
|
throw new Error('Unable to parse auth header') if name.length == part.length
|
||||||
|
value = part.slice(name.length + 1)
|
||||||
|
if not /^\s*"/.test(value)
|
||||||
|
value = value.trim()
|
||||||
|
else
|
||||||
|
while not /[^\\]"\s*$/.test(value)
|
||||||
|
p = parts.shift()
|
||||||
|
throw new Error('Unable to parse auth header') if not p?
|
||||||
|
value += ',' + p
|
||||||
|
|
||||||
|
try
|
||||||
|
value = JSON.parse(value)
|
||||||
|
catch
|
||||||
|
throw new Error('Unable to parse auth header')
|
||||||
|
|
||||||
|
res[name.trim()] = value
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,310 +0,0 @@
|
|||||||
###
|
|
||||||
# Copyright 2013-2017 Zaid Abdulla
|
|
||||||
#
|
|
||||||
# This file is part of GenieACS.
|
|
||||||
#
|
|
||||||
# GenieACS is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as
|
|
||||||
# published by the Free Software Foundation, either version 3 of the
|
|
||||||
# License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# GenieACS is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
###
|
|
||||||
|
|
||||||
vm = require 'vm'
|
|
||||||
crypto = require 'crypto'
|
|
||||||
|
|
||||||
later = require 'later'
|
|
||||||
|
|
||||||
db = require './db'
|
|
||||||
query = require './query'
|
|
||||||
|
|
||||||
|
|
||||||
REFRESH = 3000
|
|
||||||
|
|
||||||
nextRefresh = 0
|
|
||||||
hash = null
|
|
||||||
presets = null
|
|
||||||
provisions = null
|
|
||||||
virtualParameters = null
|
|
||||||
files = null
|
|
||||||
|
|
||||||
|
|
||||||
UNLOCK_SCRIPT = 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end'
|
|
||||||
EXTEND_SCRIPT = 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("pexpire", KEYS[1], ARGV[2]) else return 0 end'
|
|
||||||
|
|
||||||
lock = (lockName, ttl, callback) ->
|
|
||||||
token = Math.random().toString(36).slice(2)
|
|
||||||
unlockOrExtend = (ttl) ->
|
|
||||||
if not ttl
|
|
||||||
db.redisClient.eval(UNLOCK_SCRIPT, 1, lockName, token, (err, res) ->
|
|
||||||
throw err or new Error('Lock expired') if err or not res
|
|
||||||
)
|
|
||||||
else
|
|
||||||
db.redisClient.eval(EXTEND_SCRIPT, 1, lockName, token, ttl, (err, res) ->
|
|
||||||
throw err or new Error('Lock expired') if err or not res
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
db.redisClient.set(lockName, token, 'NX', 'PX', ttl, (err, res) ->
|
|
||||||
if err or res
|
|
||||||
return callback(err, unlockOrExtend)
|
|
||||||
|
|
||||||
setTimeout(() ->
|
|
||||||
return lock(lockName, ttl, callback)
|
|
||||||
, 200)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
computeHash = () ->
|
|
||||||
# MD5 hash for presets, provisions, virtual parameters for detecting changes
|
|
||||||
h = crypto.createHash('md5')
|
|
||||||
for p in presets
|
|
||||||
h.update(JSON.stringify(p.name))
|
|
||||||
h.update(JSON.stringify(p.channel))
|
|
||||||
h.update(JSON.stringify(p.schedule))
|
|
||||||
h.update(JSON.stringify(p.events))
|
|
||||||
h.update(JSON.stringify(p.precondition))
|
|
||||||
h.update(JSON.stringify(p.provisions))
|
|
||||||
|
|
||||||
keys = Object.keys(provisions).sort()
|
|
||||||
h.update(JSON.stringify(keys))
|
|
||||||
h.update(provisions[k].md5) for k in keys
|
|
||||||
|
|
||||||
keys = Object.keys(virtualParameters).sort()
|
|
||||||
h.update(JSON.stringify(keys))
|
|
||||||
h.update(virtualParameters[k].md5) for k in keys
|
|
||||||
|
|
||||||
hash = h.digest('hex')
|
|
||||||
|
|
||||||
|
|
||||||
refresh = (callback) ->
|
|
||||||
now = Date.now()
|
|
||||||
db.redisClient.get('presets_hash', (err, res) ->
|
|
||||||
return callback(err) if err
|
|
||||||
|
|
||||||
if hash? and res == hash
|
|
||||||
nextRefresh = now + REFRESH
|
|
||||||
return callback()
|
|
||||||
|
|
||||||
lock('presets_hash_lock', 3000, (err, unlockOrExtend) ->
|
|
||||||
return callback(err) if err
|
|
||||||
counter = 3
|
|
||||||
|
|
||||||
counter += 2
|
|
||||||
db.presetsCollection.find().toArray((err, res) ->
|
|
||||||
if err
|
|
||||||
callback(err) if counter & 1
|
|
||||||
return counter = 0
|
|
||||||
|
|
||||||
db.objectsCollection.find().toArray((err, objects) ->
|
|
||||||
if err
|
|
||||||
callback(err) if (counter & 1)
|
|
||||||
return counter = 0
|
|
||||||
|
|
||||||
res.sort((a, b) ->
|
|
||||||
if a.weight == b.weight
|
|
||||||
return a._id > b._id
|
|
||||||
else
|
|
||||||
return a.weight - b.weight
|
|
||||||
)
|
|
||||||
|
|
||||||
presets = []
|
|
||||||
|
|
||||||
for preset in res
|
|
||||||
schedule = null
|
|
||||||
if preset.schedule
|
|
||||||
parts = preset.schedule.trim().split(/\s+/)
|
|
||||||
schedule = {
|
|
||||||
md5: crypto.createHash('md5').update(preset.schedule).digest('hex')
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
schedule.duration = +(parts.shift()) * 1000
|
|
||||||
parts.unshift('0') if parts.length == 5
|
|
||||||
# TODO later.js doesn't throw erorr if expression is invalid!
|
|
||||||
schedule.schedule = later.schedule(later.parse.cron(parts.join(' '), true))
|
|
||||||
catch err
|
|
||||||
# TODO show a warning
|
|
||||||
schedule.schedule = false
|
|
||||||
|
|
||||||
events = preset.events ? {}
|
|
||||||
|
|
||||||
precondition = query.convertMongoQueryToFilters(JSON.parse(preset.precondition))
|
|
||||||
|
|
||||||
_provisions = preset.provisions or []
|
|
||||||
|
|
||||||
# Generate provisions from the old configuration format
|
|
||||||
for c in preset.configurations
|
|
||||||
switch c.type
|
|
||||||
when 'age'
|
|
||||||
_provisions.push(['refresh', c.name, c.age])
|
|
||||||
when 'value'
|
|
||||||
_provisions.push(['value', c.name, c.value])
|
|
||||||
when 'add_tag'
|
|
||||||
_provisions.push(['tag', c.tag, true])
|
|
||||||
when 'delete_tag'
|
|
||||||
_provisions.push(['tag', c.tag, false])
|
|
||||||
when 'provision'
|
|
||||||
_provisions.push([c.name].concat(c.args or []))
|
|
||||||
when 'add_object'
|
|
||||||
for obj in objects
|
|
||||||
if obj['_id'] == c.object
|
|
||||||
alias = ("#{k}:#{JSON.stringify(obj[k])}" for k in obj['_keys']).join(',')
|
|
||||||
p = "#{c.name}.[#{alias}]"
|
|
||||||
_provisions.push(['instances', p, 1])
|
|
||||||
for k of obj
|
|
||||||
if k[0] != '_' and k not in obj['_keys']
|
|
||||||
_provisions.push(['value', "#{p}.#{k}", obj[k]])
|
|
||||||
when 'delete_object'
|
|
||||||
for obj in objects
|
|
||||||
if obj['_id'] == c.object
|
|
||||||
alias = ("#{k}:#{JSON.stringify(obj[k])}" for k in obj['_keys']).join(',')
|
|
||||||
p = "#{c.name}.[#{alias}]"
|
|
||||||
_provisions.push(['instances', p, 0])
|
|
||||||
else
|
|
||||||
callback(new Error("Unknown configuration type #{c.type}")) if counter & 1
|
|
||||||
return counter = 0
|
|
||||||
|
|
||||||
presets.push({name: preset._id, channel: preset.channel or 'default', schedule: schedule, events: events, precondition: precondition, provisions: _provisions})
|
|
||||||
|
|
||||||
if (counter -= 2) == 1
|
|
||||||
computeHash()
|
|
||||||
db.redisClient.setex("presets_hash", 300, hash, (err) ->
|
|
||||||
unlockOrExtend(0)
|
|
||||||
nextRefresh = now + REFRESH
|
|
||||||
return callback()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
counter += 2
|
|
||||||
db.provisionsCollection.find().toArray((err, res) ->
|
|
||||||
if err
|
|
||||||
callback(err) if counter & 1
|
|
||||||
return counter = 0
|
|
||||||
|
|
||||||
provisions = {}
|
|
||||||
for r in res
|
|
||||||
provisions[r._id] = {}
|
|
||||||
provisions[r._id].md5 = crypto.createHash('md5').update(r.script).digest('hex')
|
|
||||||
provisions[r._id].script = new vm.Script("\"use strict\";(function(){\n#{r.script}\n})();", {filename: r._id, lineOffset: -1, timeout: 50})
|
|
||||||
|
|
||||||
if (counter -= 2) == 1
|
|
||||||
computeHash()
|
|
||||||
db.redisClient.setex("presets_hash", 300, hash, (err) ->
|
|
||||||
unlockOrExtend(0)
|
|
||||||
nextRefresh = now + REFRESH
|
|
||||||
return callback()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
counter += 2
|
|
||||||
db.virtualParametersCollection.find().toArray((err, res) ->
|
|
||||||
if err
|
|
||||||
callback(err) if counter & 1
|
|
||||||
return counter = 0
|
|
||||||
|
|
||||||
virtualParameters = {}
|
|
||||||
for r in res
|
|
||||||
virtualParameters[r._id] = {}
|
|
||||||
virtualParameters[r._id].md5 = crypto.createHash('md5').update(r.script).digest('hex')
|
|
||||||
virtualParameters[r._id].script = new vm.Script("\"use strict\";(function(){\n#{r.script}\n})();", {filename: r._id, lineOffset: -1, timeout: 50})
|
|
||||||
|
|
||||||
if (counter -= 2) == 1
|
|
||||||
computeHash()
|
|
||||||
db.redisClient.setex("presets_hash", 300, hash, (err) ->
|
|
||||||
unlockOrExtend(0)
|
|
||||||
nextRefresh = now + REFRESH
|
|
||||||
return callback()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
counter += 2
|
|
||||||
db.filesCollection.find().toArray((err, res) ->
|
|
||||||
if err
|
|
||||||
callback(err) if counter & 1
|
|
||||||
return counter = 0
|
|
||||||
|
|
||||||
files = {}
|
|
||||||
for r in res
|
|
||||||
id = r.filename or r._id.toString()
|
|
||||||
files[id] = {}
|
|
||||||
files[id].length = r.length
|
|
||||||
files[id].md5 = r.md5
|
|
||||||
files[id].contentType = r.contentType
|
|
||||||
|
|
||||||
if (counter -= 2) == 1
|
|
||||||
computeHash()
|
|
||||||
db.redisClient.setex("presets_hash", 300, hash, (err) ->
|
|
||||||
unlockOrExtend(0)
|
|
||||||
nextRefresh = now + REFRESH
|
|
||||||
return callback()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (counter -= 2) == 1
|
|
||||||
computeHash()
|
|
||||||
db.redisClient.setex("presets_hash", 300, hash, (err) ->
|
|
||||||
unlockOrExtend(0)
|
|
||||||
nextRefresh = now + REFRESH
|
|
||||||
return callback()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
getPresets = (callback) ->
|
|
||||||
if Date.now() < nextRefresh
|
|
||||||
return callback(null, hash, presets)
|
|
||||||
|
|
||||||
refresh((err) ->
|
|
||||||
return callback(err, hash, presets)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
getProvisions = (callback) ->
|
|
||||||
if Date.now() < nextRefresh
|
|
||||||
return callback(null, hash, provisions)
|
|
||||||
|
|
||||||
refresh((err) ->
|
|
||||||
return callback(err, hash, provisions)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
getVirtualParameters = (callback) ->
|
|
||||||
if Date.now() < nextRefresh
|
|
||||||
return callback(null, hash, virtualParameters)
|
|
||||||
|
|
||||||
refresh((err) ->
|
|
||||||
return callback(err, hash, virtualParameters)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
getFiles = (callback) ->
|
|
||||||
if Date.now() < nextRefresh
|
|
||||||
return callback(null, hash, files)
|
|
||||||
|
|
||||||
refresh((err) ->
|
|
||||||
return callback(err, hash, files)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
getProvisionsAndVirtualParameters = (callback) ->
|
|
||||||
if Date.now() < nextRefresh
|
|
||||||
return callback(null, hash, provisions, virtualParameters)
|
|
||||||
|
|
||||||
refresh((err) ->
|
|
||||||
return callback(err, hash, provisions, virtualParameters)
|
|
||||||
)
|
|
||||||
|
|
||||||
exports.getPresets = getPresets
|
|
||||||
exports.getProvisions = getProvisions
|
|
||||||
exports.getFiles = getFiles
|
|
||||||
exports.getProvisionsAndVirtualParameters = getProvisionsAndVirtualParameters
|
|
||||||
+257
@@ -0,0 +1,257 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2013-2017 Zaid Abdulla
|
||||||
|
*
|
||||||
|
* This file is part of GenieACS.
|
||||||
|
*
|
||||||
|
* GenieACS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* GenieACS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const config = require("./config");
|
||||||
|
|
||||||
|
const MAX_CACHE_TTL = config.get("MAX_CACHE_TTL");
|
||||||
|
|
||||||
|
let redisClient;
|
||||||
|
let mongoDb;
|
||||||
|
let mongoCollection;
|
||||||
|
let mongoTimeOffset = 0;
|
||||||
|
|
||||||
|
|
||||||
|
function connect(callback) {
|
||||||
|
const REDIS_HOST = config.get("REDIS_HOST");
|
||||||
|
|
||||||
|
if (REDIS_HOST) {
|
||||||
|
const redis = require("redis");
|
||||||
|
const REDIS_PORT = config.get("REDIS_PORT");
|
||||||
|
const REDIS_DB = config.get("REDIS_DB");
|
||||||
|
redisClient = redis.createClient(REDIS_PORT, REDIS_HOST);
|
||||||
|
redisClient.select(REDIS_DB, function(err) {
|
||||||
|
exports.get = redisGet;
|
||||||
|
exports.set = redisSet;
|
||||||
|
exports.del = redisDel;
|
||||||
|
exports.pop = redisPop;
|
||||||
|
exports.lock = redisLock;
|
||||||
|
callback(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const mongodb = require("mongodb");
|
||||||
|
const MONGODB_CONNECTION_URL = config.get("MONGODB_CONNECTION_URL");
|
||||||
|
|
||||||
|
mongodb.MongoClient.connect(MONGODB_CONNECTION_URL, function(err, db) {
|
||||||
|
if (err)
|
||||||
|
return callback(err);
|
||||||
|
|
||||||
|
mongoDb = db;
|
||||||
|
mongoCollection = db.collection("cache");
|
||||||
|
mongoCollection.ensureIndex({"expire": 1}, {expireAfterSeconds: 0});
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
mongoDb.command({hostInfo: 1}, function(err, res) {
|
||||||
|
if (err)
|
||||||
|
return callback(err);
|
||||||
|
mongoTimeOffset = res.system.currentTime.getTime() - now;
|
||||||
|
exports.get = mongoGet;
|
||||||
|
exports.set = mongoSet;
|
||||||
|
exports.del = mongoDel;
|
||||||
|
exports.pop = mongoPop;
|
||||||
|
exports.lock = mongoLock;
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function disconnect() {
|
||||||
|
if (redisClient)
|
||||||
|
redisClient.quit();
|
||||||
|
|
||||||
|
if (mongoDb)
|
||||||
|
mongoDb.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function redisGet(key, callback) {
|
||||||
|
if (Array.isArray(key))
|
||||||
|
redisClient.mget(key, callback);
|
||||||
|
else
|
||||||
|
redisClient.get(key, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function redisDel(key, callback) {
|
||||||
|
redisClient.del(key, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function redisSet(key, value, ttl, callback) {
|
||||||
|
if (!callback && typeof v === "function") {
|
||||||
|
callback = ttl;
|
||||||
|
ttl = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ttl)
|
||||||
|
ttl = MAX_CACHE_TTL;
|
||||||
|
|
||||||
|
redisClient.setex(key, ttl, value, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function redisPop(key, callback) {
|
||||||
|
const POP_SCRIPT = "local v=redis.call(\"get\",KEYS[1]);redis.call(\"del\",KEYS[1]);return v;";
|
||||||
|
redisClient.eval(POP_SCRIPT, 1, key, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function redisLock(lockName, ttl, callback) {
|
||||||
|
const UNLOCK_SCRIPT = "if redis.call(\"get\", KEYS[1]) == ARGV[1] then return redis.call(\"del\", KEYS[1]) else return 0 end";
|
||||||
|
const EXTEND_SCRIPT = "if redis.call(\"get\", KEYS[1]) == ARGV[1] then return redis.call(\"pexpire\", KEYS[1], ARGV[2]) else return 0 end";
|
||||||
|
const token = Math.random().toString(36).slice(2);
|
||||||
|
|
||||||
|
function unlockOrExtend(ttl) {
|
||||||
|
if (!ttl) {
|
||||||
|
redisClient.eval(UNLOCK_SCRIPT, 1, lockName, token, function(err, res) {
|
||||||
|
if (err || !res)
|
||||||
|
throw err || new Error("Lock expired");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
redisClient.eval(EXTEND_SCRIPT, 1, lockName, token, ttl, function(err, res) {
|
||||||
|
if (err || !res)
|
||||||
|
throw err || new Error("Lock expired");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redisClient.set(lockName, token, "NX", "EX", ttl, function(err, res) {
|
||||||
|
if (err || res)
|
||||||
|
return callback(err, unlockOrExtend);
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
redisLock(lockName, ttl, callback);
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function mongoGet(key, callback) {
|
||||||
|
let expire = new Date(Date.now() - mongoTimeOffset);
|
||||||
|
if (Array.isArray(key))
|
||||||
|
mongoCollection.find({"_id": {"$in": key}}).toArray(function(err, res) {
|
||||||
|
if (err)
|
||||||
|
return callback(err);
|
||||||
|
|
||||||
|
let indices = {};
|
||||||
|
key.forEach(function(v, i) {
|
||||||
|
indices[v] = i;
|
||||||
|
});
|
||||||
|
|
||||||
|
let values = [];
|
||||||
|
res.forEach(function(r) {
|
||||||
|
if (r["expire"] > expire)
|
||||||
|
values[indices[r["_id"]]] = r["value"];
|
||||||
|
});
|
||||||
|
callback(null, values);
|
||||||
|
});
|
||||||
|
else
|
||||||
|
mongoCollection.findOne({"_id": {"$in": [key]}}, function(err, res) {
|
||||||
|
if (err || !res)
|
||||||
|
return callback(err);
|
||||||
|
|
||||||
|
if (res["expire"] > expire)
|
||||||
|
return callback(null, res["value"]);
|
||||||
|
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function mongoDel(key, callback) {
|
||||||
|
if (Array.isArray(key))
|
||||||
|
mongoCollection.remove({"_id": {"$in": key}}, callback);
|
||||||
|
else
|
||||||
|
mongoCollection.remove({"_id": key}, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function mongoSet(key, value, ttl, callback) {
|
||||||
|
if (!callback && typeof v === "function") {
|
||||||
|
callback = ttl;
|
||||||
|
ttl = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ttl)
|
||||||
|
ttl = MAX_CACHE_TTL;
|
||||||
|
|
||||||
|
let expire = new Date((Date.now() - mongoTimeOffset) + (ttl * 1000));
|
||||||
|
mongoCollection.save({"_id": key, "value": value, "expire": expire}, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function mongoPop(key, callback) {
|
||||||
|
mongoCollection.findAndModify({"_id": key}, null, null, {remove: true}, function(err, res) {
|
||||||
|
if (err || !res["value"])
|
||||||
|
return callback(err);
|
||||||
|
|
||||||
|
if (res["value"]["expire"] > new Date(Date.now() - mongoTimeOffset))
|
||||||
|
return callback(null, res["value"]["value"]);
|
||||||
|
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function mongoLock(lockName, ttl, callback) {
|
||||||
|
const token = Math.random().toString(36).slice(2);
|
||||||
|
|
||||||
|
function unlockOrExtend(ttl) {
|
||||||
|
if (!ttl) {
|
||||||
|
mongoCollection.remove({"_id": lockName, "value": token}, function(err, res) {
|
||||||
|
if (err || res["result"]["n"] !== 1)
|
||||||
|
throw err || new Error("Lock expired");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let expire = new Date((Date.now() - mongoTimeOffset) + (ttl * 1000));
|
||||||
|
mongoCollection.update(
|
||||||
|
{"_id": lockName, "value": token},
|
||||||
|
{"expire": expire},
|
||||||
|
function(err, res) {
|
||||||
|
if (err || res["result"]["n"] !== 1)
|
||||||
|
throw err | new Error("Lock expired");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let expireTest = new Date(Date.now() - mongoTimeOffset);
|
||||||
|
let expireSet = new Date((Date.now() - mongoTimeOffset) + (ttl * 1000));
|
||||||
|
|
||||||
|
mongoCollection.update(
|
||||||
|
{"_id": lockName, "expire": {"$lte": expireTest}},
|
||||||
|
{"$set": {"value" : token, "expire": expireSet}},
|
||||||
|
{upsert: true},
|
||||||
|
function(err, res) {
|
||||||
|
if (err && err.code === 11000)
|
||||||
|
return setTimeout(function() {mongoLock(lockName, ttl, callback)}, 200);
|
||||||
|
|
||||||
|
return callback(err, unlockOrExtend);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
exports.connect = connect;
|
||||||
|
exports.disconnect = disconnect;
|
||||||
+51
-13
@@ -20,14 +20,17 @@
|
|||||||
|
|
||||||
const cluster = require("cluster");
|
const cluster = require("cluster");
|
||||||
const logger = require("./logger");
|
const logger = require("./logger");
|
||||||
|
const config = require("./config");
|
||||||
|
|
||||||
const VERSION = require("../package.json").version;
|
const VERSION = require("../package.json").version;
|
||||||
|
|
||||||
|
let respawnTimestamp = 0;
|
||||||
|
let tooManyCrashesTimestamp = 0;
|
||||||
|
|
||||||
function getDependencyVersions() {
|
function getDependencyVersions() {
|
||||||
let dependencies = [`node@${process.versions.node}`];
|
let dependencies = [`node@${process.versions.node}`];
|
||||||
try {
|
try {
|
||||||
let npmls = JSON.parse(require("child_process").execSync("npm ls --prod --json"));
|
let npmls = JSON.parse(require("child_process").execSync("npm ls --prod --json --depth 0"));
|
||||||
|
|
||||||
for (let k in npmls.dependencies)
|
for (let k in npmls.dependencies)
|
||||||
dependencies.push(`${k}@${npmls.dependencies[k].version}`);
|
dependencies.push(`${k}@${npmls.dependencies[k].version}`);
|
||||||
@@ -41,8 +44,13 @@ function getConfig() {
|
|||||||
let conf = [];
|
let conf = [];
|
||||||
const keys = Object.keys(process.env);
|
const keys = Object.keys(process.env);
|
||||||
for (let k of keys)
|
for (let k of keys)
|
||||||
if (k.startsWith("GENIEACS_"))
|
if (k.startsWith("GENIEACS_")) {
|
||||||
conf.push(`${k.slice(9)}=${process.env[k]}`);
|
const name = k.slice(9);
|
||||||
|
const value = process.env[k];
|
||||||
|
const def = config.getDefault(name);
|
||||||
|
if (def == null || value !== `${def}`)
|
||||||
|
conf.push(`${name}=${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
return conf.join(",");
|
return conf.join(",");
|
||||||
}
|
}
|
||||||
@@ -61,13 +69,40 @@ function restartWorker(worker, code, signal) {
|
|||||||
msg.signal = signal;
|
msg.signal = signal;
|
||||||
|
|
||||||
logger.error(msg);
|
logger.error(msg);
|
||||||
cluster.fork();
|
|
||||||
|
const now = Date.now();
|
||||||
|
respawnTimestamp = Math.max(now, respawnTimestamp + 2000);
|
||||||
|
if (respawnTimestamp === now) {
|
||||||
|
tooManyCrashesTimestamp = now;
|
||||||
|
cluster.fork();
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now - tooManyCrashesTimestamp > 60000) {
|
||||||
|
process.exitCode = 1;
|
||||||
|
cluster.removeListener("exit", restartWorker);
|
||||||
|
for (let pid in cluster.workers)
|
||||||
|
cluster.workers[pid].kill();
|
||||||
|
|
||||||
|
logger.error({
|
||||||
|
message: "Too many crashes, exiting",
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
if (process.exitCode) return;
|
||||||
|
cluster.fork();
|
||||||
|
}, respawnTimestamp - now);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function start(service) {
|
function start(service) {
|
||||||
logger.init(service, VERSION);
|
logger.init(service, VERSION);
|
||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
|
const SERVICE_ADDRESS = config.get(`${service.toUpperCase()}_INTERFACE`)
|
||||||
|
const SERVICE_PORT = config.get(`${service.toUpperCase()}_PORT`)
|
||||||
|
|
||||||
logger.info({
|
logger.info({
|
||||||
message: `GenieACS (genieacs-${service}) starting`,
|
message: `GenieACS (genieacs-${service}) starting`,
|
||||||
@@ -78,12 +113,15 @@ function start(service) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
cluster.on("listening", function(worker, address) {
|
cluster.on("listening", function(worker, address) {
|
||||||
logger.info({
|
if ((address.addressType === 4 || address.addressType === 6) &&
|
||||||
message: "Worker listening",
|
address.address === SERVICE_ADDRESS && address.port === SERVICE_PORT) {
|
||||||
pid: worker.process.pid,
|
logger.info({
|
||||||
address: address.address,
|
message: "Worker listening",
|
||||||
port: address.port
|
pid: worker.process.pid,
|
||||||
});
|
address: address.address,
|
||||||
|
port: address.port
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
cluster.on("exit", restartWorker);
|
cluster.on("exit", restartWorker);
|
||||||
@@ -126,13 +164,13 @@ function start(service) {
|
|||||||
process.on("uncaughtException", function(err) {
|
process.on("uncaughtException", function(err) {
|
||||||
logger.error({
|
logger.error({
|
||||||
message: "Uncaught exception in master process, exiting",
|
message: "Uncaught exception in master process, exiting",
|
||||||
exception: err
|
exception: err,
|
||||||
|
pid: process.pid
|
||||||
});
|
});
|
||||||
|
process.exitCode = 1;
|
||||||
cluster.removeListener("exit", restartWorker);
|
cluster.removeListener("exit", restartWorker);
|
||||||
for (let pid in cluster.workers)
|
for (let pid in cluster.workers)
|
||||||
cluster.workers[pid].kill();
|
cluster.workers[pid].kill();
|
||||||
|
|
||||||
logger.close();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
###
|
###
|
||||||
|
|
||||||
buffer = require 'buffer'
|
buffer = require 'buffer'
|
||||||
querystring = require 'querystring'
|
|
||||||
|
|
||||||
UNDEFINED_TYPE = '[object Undefined]'
|
UNDEFINED_TYPE = '[object Undefined]'
|
||||||
NULL_TYPE = '[object Null]'
|
NULL_TYPE = '[object Null]'
|
||||||
@@ -63,19 +62,6 @@ exports.getParamValueFromPath = (obj, path) ->
|
|||||||
return undefined
|
return undefined
|
||||||
|
|
||||||
|
|
||||||
exports.matchType = (src, dst) ->
|
|
||||||
switch typeOf(src)
|
|
||||||
when STRING_TYPE
|
|
||||||
String(dst)
|
|
||||||
when NUMBER_TYPE
|
|
||||||
Number(dst)
|
|
||||||
when BOOLEAN_TYPE
|
|
||||||
v = String(dst).trim().toLowerCase()
|
|
||||||
v == 'true' or v == 'on' or v == 'yes' or v == '1'
|
|
||||||
else
|
|
||||||
dst
|
|
||||||
|
|
||||||
|
|
||||||
pathOverlap = (a, b, start) ->
|
pathOverlap = (a, b, start) ->
|
||||||
if a.length == b.length
|
if a.length == b.length
|
||||||
res = 3
|
res = 3
|
||||||
@@ -223,28 +209,11 @@ addPathMeta = (path) ->
|
|||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
hammingWeight = (flags) ->
|
|
||||||
flags -= ((flags >> 1) & 0x55555555)
|
|
||||||
flags = (((flags >> 2) & 0x33333333) + (flags & 0x33333333))
|
|
||||||
flags = (((flags >> 4) + flags) & 0x0f0f0f0f)
|
|
||||||
flags += (flags >> 8)
|
|
||||||
flags += (flags >> 16)
|
|
||||||
return flags & 0x0000003f
|
|
||||||
|
|
||||||
|
|
||||||
# Source: http://stackoverflow.com/a/6969486
|
# Source: http://stackoverflow.com/a/6969486
|
||||||
escapeRegExp = (str) ->
|
escapeRegExp = (str) ->
|
||||||
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&")
|
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&")
|
||||||
|
|
||||||
|
|
||||||
# djb2 hashing algorithm adapted from http://www.cse.yorku.ca/~oz/hash.html
|
|
||||||
djb2 = (str) ->
|
|
||||||
hash = 5381
|
|
||||||
for i in [0...str.length]
|
|
||||||
hash = ((hash << 5) + hash) ^ str.charCodeAt(i)
|
|
||||||
return hash
|
|
||||||
|
|
||||||
|
|
||||||
exports.UNDEFINED_TYPE = UNDEFINED_TYPE
|
exports.UNDEFINED_TYPE = UNDEFINED_TYPE
|
||||||
exports.NULL_TYPE = NULL_TYPE
|
exports.NULL_TYPE = NULL_TYPE
|
||||||
exports.NUMBER_TYPE = NUMBER_TYPE
|
exports.NUMBER_TYPE = NUMBER_TYPE
|
||||||
@@ -257,6 +226,4 @@ exports.typeOf = typeOf
|
|||||||
exports.pathOverlap = pathOverlap
|
exports.pathOverlap = pathOverlap
|
||||||
exports.parsePath = parsePath
|
exports.parsePath = parsePath
|
||||||
exports.addPathMeta = addPathMeta
|
exports.addPathMeta = addPathMeta
|
||||||
exports.hammingWeight = hammingWeight
|
|
||||||
exports.escapeRegExp = escapeRegExp
|
exports.escapeRegExp = escapeRegExp
|
||||||
exports.djb2 = djb2
|
|
||||||
+17
-1
@@ -24,7 +24,7 @@ options = {
|
|||||||
CONFIG_DIR : {type : 'path', default : 'config'},
|
CONFIG_DIR : {type : 'path', default : 'config'},
|
||||||
MONGODB_CONNECTION_URL : {type : 'string', default : 'mongodb://127.0.0.1/genieacs'},
|
MONGODB_CONNECTION_URL : {type : 'string', default : 'mongodb://127.0.0.1/genieacs'},
|
||||||
REDIS_PORT : {type : 'int', default : 6379},
|
REDIS_PORT : {type : 'int', default : 6379},
|
||||||
REDIS_HOST : {type : 'string', default : '127.0.0.1'},
|
REDIS_HOST : {type : 'string', default : ''},
|
||||||
REDIS_DB : {type : 'int', default : 0},
|
REDIS_DB : {type : 'int', default : 0},
|
||||||
|
|
||||||
CWMP_WORKER_PROCESSES : {type : 'int', default : 0},
|
CWMP_WORKER_PROCESSES : {type : 'int', default : 0},
|
||||||
@@ -49,6 +49,8 @@ options = {
|
|||||||
FS_LOG_FILE : {type: 'path', default : ''},
|
FS_LOG_FILE : {type: 'path', default : ''},
|
||||||
FS_ACCESS_LOG_FILE : {type : 'path', default : ''},
|
FS_ACCESS_LOG_FILE : {type : 'path', default : ''},
|
||||||
|
|
||||||
|
UDP_CONNECTION_REQUEST_PORT : {type : 'int', default : 0},
|
||||||
|
|
||||||
DOWNLOAD_TIMEOUT: {type : 'int', default : 3600},
|
DOWNLOAD_TIMEOUT: {type : 'int', default : 3600},
|
||||||
EXT_TIMEOUT: {type: 'int', default: 3000},
|
EXT_TIMEOUT: {type: 'int', default: 3000},
|
||||||
MAX_CACHE_TTL : {type : 'int', default : 86400},
|
MAX_CACHE_TTL : {type : 'int', default : 86400},
|
||||||
@@ -62,6 +64,10 @@ options = {
|
|||||||
COOKIES_PATH : {type : 'string'},
|
COOKIES_PATH : {type : 'string'},
|
||||||
LOG_FORMAT : {type : 'string', default : 'simple'},
|
LOG_FORMAT : {type : 'string', default : 'simple'},
|
||||||
ACCESS_LOG_FORMAT : {type : 'string', default : ''},
|
ACCESS_LOG_FORMAT : {type : 'string', default : ''},
|
||||||
|
MAX_CONCURRENT_REQUESTS : {type : 'int', default: 20},
|
||||||
|
DATETIME_MILLISECONDS : {type: 'bool', default: true},
|
||||||
|
BOOLEAN_LITERAL : {type: 'bool', default: true},
|
||||||
|
CONNECTION_REQUEST_ALLOW_BASIC_AUTH: {type: 'bool', default: false}
|
||||||
|
|
||||||
# XML configuration
|
# XML configuration
|
||||||
XML_RECOVER : {type : 'bool', default : false},
|
XML_RECOVER : {type : 'bool', default : false},
|
||||||
@@ -185,8 +191,18 @@ get = (option, deviceId) ->
|
|||||||
return allConfig[option]
|
return allConfig[option]
|
||||||
|
|
||||||
|
|
||||||
|
getDefault = (optionName) ->
|
||||||
|
option = options[optionName]
|
||||||
|
return null if not option
|
||||||
|
val = option.default
|
||||||
|
if val and option.type == 'path'
|
||||||
|
val = path.resolve(val)
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
# load authentication scripts
|
# load authentication scripts
|
||||||
try
|
try
|
||||||
exports.auth = require(path.resolve(allConfig.CONFIG_DIR, 'auth'))
|
exports.auth = require(path.resolve(allConfig.CONFIG_DIR, 'auth'))
|
||||||
|
|
||||||
exports.get = get
|
exports.get = get
|
||||||
|
exports.getDefault = getDefault
|
||||||
+523
-281
@@ -28,27 +28,63 @@ session = require './session'
|
|||||||
query = require './query'
|
query = require './query'
|
||||||
device = require './device'
|
device = require './device'
|
||||||
cache = require './cache'
|
cache = require './cache'
|
||||||
|
localCache = require './local-cache'
|
||||||
db = require './db'
|
db = require './db'
|
||||||
logger = require './logger'
|
logger = require './logger'
|
||||||
|
scheduling = require './scheduling'
|
||||||
|
|
||||||
|
|
||||||
MAX_CYCLES = 4
|
MAX_CYCLES = 4
|
||||||
|
|
||||||
|
MAX_CONCURRENT_REQUESTS = config.get('MAX_CONCURRENT_REQUESTS')
|
||||||
|
stats = {
|
||||||
|
concurrentRequests: 0
|
||||||
|
totalRequests: 0
|
||||||
|
droppedRequests: 0
|
||||||
|
initiatedSessions: 0
|
||||||
|
}
|
||||||
|
|
||||||
throwError = (err, httpResponse) ->
|
throwError = (err, httpResponse) ->
|
||||||
if httpResponse
|
if httpResponse
|
||||||
|
currentSessions.delete(httpResponse.connection)
|
||||||
httpResponse.writeHead(500, {'Connection' : 'close'})
|
httpResponse.writeHead(500, {'Connection' : 'close'})
|
||||||
httpResponse.end("#{err.name}: #{err.message}")
|
httpResponse.end("#{err.name}: #{err.message}")
|
||||||
|
stats.concurrentRequests -= 1
|
||||||
|
|
||||||
throw err
|
throw err
|
||||||
|
|
||||||
|
|
||||||
writeResponse = (sessionContext, res) ->
|
writeResponse = (sessionContext, res, close) ->
|
||||||
|
# Close connection after last request in session
|
||||||
|
res.headers['Connection'] = 'close' if close
|
||||||
|
|
||||||
if config.get('DEBUG', sessionContext.deviceId)
|
if config.get('DEBUG', sessionContext.deviceId)
|
||||||
dump = "# RESPONSE #{new Date(Date.now())}\n" + JSON.stringify(res.headers) + "\n#{res.data}\n\n"
|
dump = "# RESPONSE #{new Date(Date.now())}\n" + JSON.stringify(res.headers) + "\n#{res.data}\n\n"
|
||||||
require('fs').appendFile("./debug/#{sessionContext.deviceId}.dump", dump, (err) ->
|
require('fs').appendFile("./debug/#{sessionContext.deviceId}.dump", dump, (err) ->
|
||||||
throwError(err) if err
|
throwError(err) if err
|
||||||
)
|
)
|
||||||
|
|
||||||
|
finish = () ->
|
||||||
|
stats.concurrentRequests -= 1
|
||||||
|
if sessionContext.httpRequest.connection.destroyed
|
||||||
|
logger.accessError({
|
||||||
|
sessionContext: sessionContext
|
||||||
|
message: 'Connection dropped'
|
||||||
|
})
|
||||||
|
else if close
|
||||||
|
endSession(sessionContext, (err, isNew) ->
|
||||||
|
return throwError(err) if err
|
||||||
|
|
||||||
|
if isNew
|
||||||
|
logger.accessInfo({
|
||||||
|
sessionContext: sessionContext
|
||||||
|
message: 'New device registered'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
else
|
||||||
|
sessionContext.lastActivity = Date.now()
|
||||||
|
currentSessions.set(sessionContext.httpRequest.connection, sessionContext)
|
||||||
|
|
||||||
# respond using the same content-encoding as the request
|
# respond using the same content-encoding as the request
|
||||||
if sessionContext.httpRequest.headers['content-encoding']? and res.data.length > 0
|
if sessionContext.httpRequest.headers['content-encoding']? and res.data.length > 0
|
||||||
switch sessionContext.httpRequest.headers['content-encoding']
|
switch sessionContext.httpRequest.headers['content-encoding']
|
||||||
@@ -65,14 +101,13 @@ writeResponse = (sessionContext, res) ->
|
|||||||
res.headers['Content-Length'] = data.length
|
res.headers['Content-Length'] = data.length
|
||||||
sessionContext.httpResponse.writeHead(res.code, res.headers)
|
sessionContext.httpResponse.writeHead(res.code, res.headers)
|
||||||
sessionContext.httpResponse.end(data)
|
sessionContext.httpResponse.end(data)
|
||||||
|
finish()
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
res.headers['Content-Length'] = res.data.length
|
res.headers['Content-Length'] = res.data.length
|
||||||
sessionContext.httpResponse.writeHead(res.code, res.headers)
|
sessionContext.httpResponse.writeHead(res.code, res.headers)
|
||||||
sessionContext.httpResponse.end(res.data)
|
sessionContext.httpResponse.end(res.data)
|
||||||
|
finish()
|
||||||
delete sessionContext.httpRequest
|
|
||||||
delete sessionContext.httpResponse
|
|
||||||
|
|
||||||
|
|
||||||
recordFault = (sessionContext, fault, provisions, channels) ->
|
recordFault = (sessionContext, fault, provisions, channels) ->
|
||||||
@@ -99,7 +134,7 @@ recordFault = (sessionContext, fault, provisions, channels) ->
|
|||||||
sessionContext.faultsTouched ?= {}
|
sessionContext.faultsTouched ?= {}
|
||||||
sessionContext.faultsTouched[channel] = true
|
sessionContext.faultsTouched[channel] = true
|
||||||
|
|
||||||
logger.accessError({
|
logger.accessWarn({
|
||||||
sessionContext: sessionContext
|
sessionContext: sessionContext
|
||||||
message: 'Channel has faulted'
|
message: 'Channel has faulted'
|
||||||
fault: fault
|
fault: fault
|
||||||
@@ -154,7 +189,7 @@ transferComplete = (sessionContext, rpc) ->
|
|||||||
for k, v of operation.retries
|
for k, v of operation.retries
|
||||||
sessionContext.retries[k] = v
|
sessionContext.retries[k] = v
|
||||||
|
|
||||||
recordFault(sessionContext, fault)
|
recordFault(sessionContext, fault, operation.provisions, operation.channels)
|
||||||
|
|
||||||
res = soap.response({
|
res = soap.response({
|
||||||
id : rpc.id,
|
id : rpc.id,
|
||||||
@@ -165,28 +200,6 @@ transferComplete = (sessionContext, rpc) ->
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
testSchedule = (schedule, timestamp) ->
|
|
||||||
range = schedule.schedule.nextRange(10, new Date(timestamp))
|
|
||||||
prev = schedule.schedule.prevRange(1, new Date(timestamp), new Date(timestamp - schedule.duration))
|
|
||||||
|
|
||||||
if prev
|
|
||||||
first = +(prev[0] ? 0)
|
|
||||||
last = +(prev[1] ? 0) + schedule.duration
|
|
||||||
else
|
|
||||||
first = +(range[0][0] ? 0)
|
|
||||||
last = +(range[0][1] ? 0) + schedule.duration
|
|
||||||
|
|
||||||
for r in range
|
|
||||||
if last < r[0]
|
|
||||||
break
|
|
||||||
last = +(r[1] ? 0) + schedule.duration
|
|
||||||
|
|
||||||
if timestamp >= first
|
|
||||||
return [true, Math.max(0, last - timestamp)]
|
|
||||||
else
|
|
||||||
return [false, first - timestamp]
|
|
||||||
|
|
||||||
|
|
||||||
# Append providions and remove duplicates
|
# Append providions and remove duplicates
|
||||||
appendProvisions = (original, toAppend) ->
|
appendProvisions = (original, toAppend) ->
|
||||||
modified = false
|
modified = false
|
||||||
@@ -213,7 +226,7 @@ appendProvisions = (original, toAppend) ->
|
|||||||
|
|
||||||
|
|
||||||
applyPresets = (sessionContext) ->
|
applyPresets = (sessionContext) ->
|
||||||
cache.getPresets((err, presetsHash, presets) ->
|
localCache.getPresets((err, presetsHash, presets) ->
|
||||||
return throwError(err, sessionContext.httpResponse) if err
|
return throwError(err, sessionContext.httpResponse) if err
|
||||||
|
|
||||||
# Filter presets based on existing faults
|
# Filter presets based on existing faults
|
||||||
@@ -237,6 +250,9 @@ applyPresets = (sessionContext) ->
|
|||||||
|
|
||||||
blackList[channel] = if fault.precondition then 1 else 2
|
blackList[channel] = if fault.precondition then 1 else 2
|
||||||
|
|
||||||
|
sessionContext.deviceData.timestamps.revision = 1
|
||||||
|
sessionContext.deviceData.attributes.revision = 1
|
||||||
|
|
||||||
deviceEvents = {}
|
deviceEvents = {}
|
||||||
for p in sessionContext.deviceData.paths.find(['Events', '*'], false, true)
|
for p in sessionContext.deviceData.paths.find(['Events', '*'], false, true)
|
||||||
if sessionContext.timestamp <= sessionContext.deviceData.attributes.get(p)?.value?[1][0]
|
if sessionContext.timestamp <= sessionContext.deviceData.attributes.get(p)?.value?[1][0]
|
||||||
@@ -257,8 +273,13 @@ applyPresets = (sessionContext) ->
|
|||||||
eventsMatch = false
|
eventsMatch = false
|
||||||
break
|
break
|
||||||
|
|
||||||
continue if (not eventsMatch) or (preset.schedule and not
|
continue if not eventsMatch
|
||||||
(preset.schedule.schedule and testSchedule(preset.schedule, sessionContext.timestamp)[0]))
|
|
||||||
|
if preset.schedule
|
||||||
|
continue if not preset.schedule.schedule
|
||||||
|
r = scheduling.cron(sessionContext.timestamp, preset.schedule.schedule)
|
||||||
|
if not (r[0] + preset.schedule.duration > sessionContext.timestamp)
|
||||||
|
continue
|
||||||
|
|
||||||
filteredPresets.push(preset)
|
filteredPresets.push(preset)
|
||||||
for k of preset.precondition
|
for k of preset.precondition
|
||||||
@@ -307,18 +328,25 @@ applyPresets = (sessionContext) ->
|
|||||||
sessionContext.faultsTouched ?= {}
|
sessionContext.faultsTouched ?= {}
|
||||||
sessionContext.faultsTouched[channel] = true
|
sessionContext.faultsTouched[channel] = true
|
||||||
|
|
||||||
sessionContext.presetCycles = (sessionContext.presetCycles or 0) + 1
|
# Don't increment when processing individual channels (e.g. after fault)
|
||||||
|
sessionContext.presetCycles = (sessionContext.presetCycles or 0) + 1 if not whiteList?
|
||||||
|
|
||||||
if sessionContext.presetCycles > MAX_CYCLES
|
if sessionContext.presetCycles > MAX_CYCLES
|
||||||
fault = {
|
fault = {
|
||||||
code: 'endless_cycle'
|
code: 'preset_loop'
|
||||||
message: 'The provision seems to be repeating indefinitely'
|
message: 'The presets are stuck in an endless configuration loop'
|
||||||
timestamp: sessionContext.timestamp
|
timestamp: sessionContext.timestamp
|
||||||
}
|
}
|
||||||
recordFault(sessionContext, fault)
|
recordFault(sessionContext, fault)
|
||||||
|
# No need to save retryNow
|
||||||
|
for c of sessionContext.faults
|
||||||
|
delete sessionContext.faults[c].retryNow
|
||||||
session.clearProvisions(sessionContext)
|
session.clearProvisions(sessionContext)
|
||||||
return sendAcsRequest(sessionContext)
|
return sendAcsRequest(sessionContext)
|
||||||
|
|
||||||
|
sessionContext.deviceData.timestamps.dirty = 0;
|
||||||
|
sessionContext.deviceData.attributes.dirty = 0;
|
||||||
|
|
||||||
session.rpcRequest(sessionContext, null, (err, fault, id, acsRequest) ->
|
session.rpcRequest(sessionContext, null, (err, fault, id, acsRequest) ->
|
||||||
return throwError(err, sessionContext.httpResponse) if err
|
return throwError(err, sessionContext.httpResponse) if err
|
||||||
|
|
||||||
@@ -337,6 +365,10 @@ applyPresets = (sessionContext) ->
|
|||||||
if whiteList?
|
if whiteList?
|
||||||
return applyPresets(sessionContext)
|
return applyPresets(sessionContext)
|
||||||
|
|
||||||
|
if sessionContext.deviceData.timestamps.dirty > 1 or
|
||||||
|
sessionContext.deviceData.attributes.dirty > 1
|
||||||
|
return applyPresets(sessionContext)
|
||||||
|
|
||||||
sendAcsRequest(sessionContext, id, acsRequest)
|
sendAcsRequest(sessionContext, id, acsRequest)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -372,112 +404,151 @@ nextRpc = (sessionContext) ->
|
|||||||
|
|
||||||
session.clearProvisions(sessionContext)
|
session.clearProvisions(sessionContext)
|
||||||
|
|
||||||
for task in sessionContext.tasks
|
# Clear expired tasks
|
||||||
|
sessionContext.tasks = sessionContext.tasks.filter((task) ->
|
||||||
|
return true if not (task.expiry <= sessionContext.timestamp)
|
||||||
|
|
||||||
|
logger.accessInfo({
|
||||||
|
sessionContext: sessionContext
|
||||||
|
message: 'Task expired'
|
||||||
|
task: task
|
||||||
|
})
|
||||||
|
sessionContext.doneTasks ?= []
|
||||||
|
sessionContext.doneTasks.push(task._id)
|
||||||
channel = "task_#{task._id}"
|
channel = "task_#{task._id}"
|
||||||
|
if channel of sessionContext.faults
|
||||||
|
delete sessionContext.faults[channel]
|
||||||
|
sessionContext.faultsTouched ?= {}
|
||||||
|
sessionContext.faultsTouched[channel] = true
|
||||||
|
|
||||||
# Delete if expired
|
return false
|
||||||
if task.expiry <= sessionContext.timestamp
|
)
|
||||||
logger.accessInfo({
|
|
||||||
sessionContext: sessionContext
|
|
||||||
message: 'Task expired'
|
|
||||||
task: task
|
|
||||||
})
|
|
||||||
sessionContext.doneTasks ?= []
|
|
||||||
sessionContext.doneTasks.push(String(task._id))
|
|
||||||
if channel of sessionContext.faults
|
|
||||||
delete sessionContext.faults[channel]
|
|
||||||
sessionContext.faultsTouched ?= {}
|
|
||||||
sessionContext.faultsTouched[channel] = true
|
|
||||||
continue
|
|
||||||
|
|
||||||
if sessionContext.faults[channel]
|
task = sessionContext.tasks.find((task) -> !sessionContext.faults["task_#{task._id}"])
|
||||||
continue
|
|
||||||
|
|
||||||
switch task.name
|
return applyPresets(sessionContext) if not task
|
||||||
when 'getParameterValues'
|
|
||||||
# Set channel in case params array is empty
|
|
||||||
sessionContext.channels["task_#{task._id}"] = 0
|
|
||||||
for p in task.parameterNames
|
|
||||||
session.addProvisions(sessionContext, "task_#{task._id}",
|
|
||||||
[['refresh', p]])
|
|
||||||
when 'setParameterValues'
|
|
||||||
# Set channel in case params array is empty
|
|
||||||
sessionContext.channels["task_#{task._id}"] = 0
|
|
||||||
for p in task.parameterValues
|
|
||||||
session.addProvisions(sessionContext, "task_#{task._id}",
|
|
||||||
[['value', p[0], p[1]]])
|
|
||||||
when 'refreshObject'
|
|
||||||
session.addProvisions(sessionContext, "task_#{task._id}",
|
|
||||||
[['refresh', task.objectName]])
|
|
||||||
when 'reboot'
|
|
||||||
session.addProvisions(sessionContext, "task_#{task._id}",
|
|
||||||
[['reboot']])
|
|
||||||
when 'factoryReset'
|
|
||||||
session.addProvisions(sessionContext, "task_#{task._id}",
|
|
||||||
[['reset']])
|
|
||||||
when 'download'
|
|
||||||
session.addProvisions(sessionContext, "task_#{task._id}",
|
|
||||||
[['download', task.fileType, task.fileName, task.targetFileName]])
|
|
||||||
when 'addObject'
|
|
||||||
alias = ("#{p[0]}:#{JSON.stringify(p[1])}" for p in task.parameterValues or []).join(',')
|
|
||||||
session.addProvisions(sessionContext, "task_#{task._id}",
|
|
||||||
[['instances', "#{task.objectName}.[#{alias}]", "+1"]]);
|
|
||||||
when 'deleteObject'
|
|
||||||
session.addProvisions(sessionContext, "task_#{task._id}",
|
|
||||||
[['instances', task.objectName, 0]]);
|
|
||||||
else
|
|
||||||
return throwError(new Error('Task name not recognized'), sessionContext.httpResponse) if err
|
|
||||||
|
|
||||||
return nextRpc(sessionContext)
|
switch task.name
|
||||||
|
when 'getParameterValues'
|
||||||
|
# Set channel in case params array is empty
|
||||||
|
sessionContext.channels["task_#{task._id}"] = 0
|
||||||
|
for p in task.parameterNames
|
||||||
|
session.addProvisions(sessionContext, "task_#{task._id}",
|
||||||
|
[['refresh', p]])
|
||||||
|
when 'setParameterValues'
|
||||||
|
# Set channel in case params array is empty
|
||||||
|
sessionContext.channels["task_#{task._id}"] = 0
|
||||||
|
for p in task.parameterValues
|
||||||
|
session.addProvisions(sessionContext, "task_#{task._id}",
|
||||||
|
[['value', p[0], p[1]]])
|
||||||
|
when 'refreshObject'
|
||||||
|
session.addProvisions(sessionContext, "task_#{task._id}",
|
||||||
|
[['refresh', task.objectName]])
|
||||||
|
when 'reboot'
|
||||||
|
session.addProvisions(sessionContext, "task_#{task._id}",
|
||||||
|
[['reboot']])
|
||||||
|
when 'factoryReset'
|
||||||
|
session.addProvisions(sessionContext, "task_#{task._id}",
|
||||||
|
[['reset']])
|
||||||
|
when 'download'
|
||||||
|
session.addProvisions(sessionContext, "task_#{task._id}",
|
||||||
|
[['download', task.fileType, task.fileName, task.targetFileName]])
|
||||||
|
when 'addObject'
|
||||||
|
alias = ("#{p[0]}:#{JSON.stringify(p[1])}" for p in task.parameterValues or []).join(',')
|
||||||
|
session.addProvisions(sessionContext, "task_#{task._id}",
|
||||||
|
[['instances', "#{task.objectName}.[#{alias}]", "+1"]]);
|
||||||
|
when 'deleteObject'
|
||||||
|
session.addProvisions(sessionContext, "task_#{task._id}",
|
||||||
|
[['instances', task.objectName, 0]]);
|
||||||
|
else
|
||||||
|
return throwError(new Error('Task name not recognized'), sessionContext.httpResponse)
|
||||||
|
|
||||||
return applyPresets(sessionContext)
|
return nextRpc(sessionContext)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
endSession = (sessionContext, callback) ->
|
||||||
|
saveCache = sessionContext.cacheUntil?
|
||||||
|
|
||||||
|
counter = 3
|
||||||
|
|
||||||
|
counter += 2
|
||||||
|
db.saveDevice(sessionContext.deviceId, sessionContext.deviceData, sessionContext.new, sessionContext.timestamp, (err) ->
|
||||||
|
if err
|
||||||
|
callback(err) if counter & 1
|
||||||
|
return counter = 0
|
||||||
|
|
||||||
|
return callback(null, sessionContext.new) if (counter -= 2) == 1
|
||||||
|
)
|
||||||
|
|
||||||
|
for k of sessionContext.operationsTouched
|
||||||
|
counter += 2
|
||||||
|
saveCache = true
|
||||||
|
if sessionContext.operations[k]?
|
||||||
|
db.saveOperation(sessionContext.deviceId, k, sessionContext.operations[k], (err) ->
|
||||||
|
if err
|
||||||
|
callback(err) if counter & 1
|
||||||
|
return counter = 0
|
||||||
|
|
||||||
|
return callback(null, sessionContext.new) if (counter -= 2) == 1
|
||||||
|
)
|
||||||
|
else
|
||||||
|
db.deleteOperation(sessionContext.deviceId, k, (err) ->
|
||||||
|
if err
|
||||||
|
callback(err) if counter & 1
|
||||||
|
return counter = 0
|
||||||
|
|
||||||
|
return callback(null, sessionContext.new) if (counter -= 2) == 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if sessionContext.doneTasks?.length
|
||||||
|
counter += 2
|
||||||
|
saveCache = true
|
||||||
|
db.clearTasks(sessionContext.deviceId, sessionContext.doneTasks, (err) ->
|
||||||
|
if err
|
||||||
|
callback(err) if counter & 1
|
||||||
|
return counter = 0
|
||||||
|
|
||||||
|
return callback(null, sessionContext.new) if (counter -= 2) == 1
|
||||||
|
)
|
||||||
|
|
||||||
|
for k of sessionContext.faultsTouched
|
||||||
|
counter += 2
|
||||||
|
saveCache = true
|
||||||
|
if sessionContext.faults[k]
|
||||||
|
sessionContext.faults[k].retries = sessionContext.retries[k]
|
||||||
|
db.saveFault(sessionContext.deviceId, k, sessionContext.faults[k], (err) ->
|
||||||
|
if err
|
||||||
|
callback(err) if counter & 1
|
||||||
|
return counter = 0
|
||||||
|
|
||||||
|
return callback(null, sessionContext.new) if (counter -= 2) == 1
|
||||||
|
)
|
||||||
|
else
|
||||||
|
db.deleteFault(sessionContext.deviceId, k, (err) ->
|
||||||
|
if err
|
||||||
|
callback(err) if counter & 1
|
||||||
|
return counter = 0
|
||||||
|
|
||||||
|
return callback(null, sessionContext.new) if (counter -= 2) == 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if saveCache
|
||||||
|
counter += 2
|
||||||
|
cacheDueTasksAndFaultsAndOperations(sessionContext.deviceId, sessionContext.tasks, sessionContext.faults, sessionContext.operations, sessionContext.cacheUntil, (err) ->
|
||||||
|
if err
|
||||||
|
callback(err) if counter & 1
|
||||||
|
return counter = 0
|
||||||
|
|
||||||
|
return callback(null, sessionContext.new) if (counter -= 2) == 1
|
||||||
|
)
|
||||||
|
|
||||||
|
callback(null, sessionContext.new) if (counter -= 2) == 1
|
||||||
|
|
||||||
|
|
||||||
sendAcsRequest = (sessionContext, id, acsRequest) ->
|
sendAcsRequest = (sessionContext, id, acsRequest) ->
|
||||||
if not acsRequest?
|
if not acsRequest?
|
||||||
session.end(sessionContext, (err, isNew) ->
|
return writeResponse(sessionContext, soap.response(null), true)
|
||||||
return throwError(err, sessionContext.httpResponse) if err
|
|
||||||
|
|
||||||
delete currentSessions.get(sessionContext.httpRequest.connection)[sessionContext.sessionId]
|
|
||||||
if isNew
|
|
||||||
logger.accessInfo({
|
|
||||||
sessionContext: sessionContext
|
|
||||||
message: 'New device registered'
|
|
||||||
})
|
|
||||||
|
|
||||||
db.clearTasks(sessionContext.deviceId, sessionContext.doneTasks, (err) ->
|
|
||||||
return throwError(err, sessionContext.httpResponse) if err
|
|
||||||
|
|
||||||
counter = 3
|
|
||||||
|
|
||||||
for k of sessionContext.faultsTouched
|
|
||||||
counter += 2
|
|
||||||
if sessionContext.faults[k]
|
|
||||||
sessionContext.faults[k].retries = sessionContext.retries[k]
|
|
||||||
db.saveFault(sessionContext.deviceId, k, sessionContext.faults[k], (err) ->
|
|
||||||
if err
|
|
||||||
throwError(err, sessionContext.httpResponse) if counter & 1
|
|
||||||
return counter = 0
|
|
||||||
|
|
||||||
if (counter -= 2) == 1
|
|
||||||
return writeResponse(sessionContext, soap.response(null))
|
|
||||||
)
|
|
||||||
else
|
|
||||||
db.deleteFault(sessionContext.deviceId, k, (err) ->
|
|
||||||
if err
|
|
||||||
throwError(err, sessionContext.httpResponse) if counter & 1
|
|
||||||
return counter = 0
|
|
||||||
|
|
||||||
if (counter -= 2) == 1
|
|
||||||
return writeResponse(sessionContext, soap.response(null))
|
|
||||||
)
|
|
||||||
|
|
||||||
if (counter -= 2) == 1
|
|
||||||
return writeResponse(sessionContext, soap.response(null))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
if acsRequest.name is 'Download'
|
if acsRequest.name is 'Download'
|
||||||
if not acsRequest.url?
|
if not acsRequest.url?
|
||||||
@@ -487,10 +558,10 @@ sendAcsRequest = (sessionContext, id, acsRequest) ->
|
|||||||
acsRequest.url = if FS_SSL then 'https://' else 'http://'
|
acsRequest.url = if FS_SSL then 'https://' else 'http://'
|
||||||
acsRequest.url += FS_HOSTNAME
|
acsRequest.url += FS_HOSTNAME
|
||||||
acsRequest.url += ":#{FS_PORT}" if FS_PORT != 80
|
acsRequest.url += ":#{FS_PORT}" if FS_PORT != 80
|
||||||
acsRequest.url += "/#{encodeURIComponent(acsRequest.fileName)}"
|
acsRequest.url += "/#{encodeURI(acsRequest.fileName)}"
|
||||||
|
|
||||||
if not acsRequest.fileSize?
|
if not acsRequest.fileSize?
|
||||||
return cache.getFiles((err, hash, files) ->
|
return localCache.getFiles((err, hash, files) ->
|
||||||
return throwError(err, sessionContext.httpResponse) if err
|
return throwError(err, sessionContext.httpResponse) if err
|
||||||
|
|
||||||
if acsRequest.fileName of files
|
if acsRequest.fileName of files
|
||||||
@@ -518,56 +589,227 @@ sendAcsRequest = (sessionContext, id, acsRequest) ->
|
|||||||
writeResponse(sessionContext, res)
|
writeResponse(sessionContext, res)
|
||||||
|
|
||||||
|
|
||||||
getSession = (httpRequest, callback) ->
|
getSession = (connection, sessionId, callback) ->
|
||||||
|
sessionContext = currentSessions.get(connection)
|
||||||
|
|
||||||
|
if sessionContext
|
||||||
|
currentSessions.delete(connection)
|
||||||
|
return callback(null, sessionContext)
|
||||||
|
|
||||||
|
return callback() if not sessionId
|
||||||
|
|
||||||
|
setTimeout(() ->
|
||||||
|
cache.pop("session_#{sessionId}", (err, sessionContextString) ->
|
||||||
|
return callback(err) if err or not sessionContextString
|
||||||
|
session.deserialize(sessionContextString, callback)
|
||||||
|
)
|
||||||
|
, 100)
|
||||||
|
|
||||||
|
|
||||||
|
currentSessions = new WeakMap()
|
||||||
|
|
||||||
|
# When socket closes, store active sessions in cache
|
||||||
|
onConnection = (socket) ->
|
||||||
|
# The property remoteAddress may be undefined after the connection is
|
||||||
|
# closed, unless we read it at least once (caching?)
|
||||||
|
socket.remoteAddress
|
||||||
|
|
||||||
|
socket.on('close', () ->
|
||||||
|
sessionContext = currentSessions.get(socket)
|
||||||
|
return if not sessionContext
|
||||||
|
currentSessions.delete(socket)
|
||||||
|
now = Date.now()
|
||||||
|
|
||||||
|
lastActivity = sessionContext.lastActivity
|
||||||
|
timeoutMsg = logger.flatten({
|
||||||
|
sessionContext: sessionContext
|
||||||
|
message: 'Session timeout'
|
||||||
|
sessionTimestamp: sessionContext.timestamp
|
||||||
|
})
|
||||||
|
|
||||||
|
timeout = (sessionContext.lastActivity + sessionContext.timeout * 1000) - now
|
||||||
|
if timeout <= 0
|
||||||
|
return logger.accessError(timeoutMsg)
|
||||||
|
|
||||||
|
setTimeout(() ->
|
||||||
|
cache.get("session_#{sessionContext.sessionId}", (err, sessionContextString) ->
|
||||||
|
return throwError(err) if err
|
||||||
|
return if not sessionContextString
|
||||||
|
session.deserialize(sessionContextString, (err, sessionContext) ->
|
||||||
|
return throwError(err) if err
|
||||||
|
if sessionContext.lastActivity == lastActivity
|
||||||
|
logger.accessError(timeoutMsg)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
, timeout + 1000).unref()
|
||||||
|
|
||||||
|
session.serialize(sessionContext, (err, sessionContextString) ->
|
||||||
|
return throwError(err) if err
|
||||||
|
cache.set("session_#{sessionContext.sessionId}", sessionContextString, Math.ceil(timeout / 1000) + 3, (err) ->
|
||||||
|
return throwError(err) if err
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
setInterval(() ->
|
||||||
|
if stats.droppedRequests
|
||||||
|
logger.warn({
|
||||||
|
message: 'Worker overloaded'
|
||||||
|
droppedRequests: stats.droppedRequests
|
||||||
|
totalRequests: stats.totalRequests
|
||||||
|
initiatedSessions: stats.initiatedSessions
|
||||||
|
pid: process.pid
|
||||||
|
})
|
||||||
|
|
||||||
|
stats.totalRequests = 0
|
||||||
|
stats.droppedRequests = 0
|
||||||
|
stats.initiatedSessions = 0
|
||||||
|
, 10000).unref()
|
||||||
|
|
||||||
|
|
||||||
|
getDueTasksAndFaultsAndOperations = (deviceId, timestamp, callback) ->
|
||||||
|
cache.get("#{deviceId}_tasks_faults_operations", (err, res) ->
|
||||||
|
return callback(err) if err
|
||||||
|
|
||||||
|
if res
|
||||||
|
res = JSON.parse(res)
|
||||||
|
return callback(null, res.tasks or [], res.faults or {}, res.operations or {})
|
||||||
|
|
||||||
|
faults = tasks = operations = cacheUntil = null
|
||||||
|
|
||||||
|
db.getFaults(deviceId, (err, _faults) ->
|
||||||
|
if err
|
||||||
|
callback?(err)
|
||||||
|
return callback = null
|
||||||
|
|
||||||
|
faults = _faults
|
||||||
|
if tasks? and operations?
|
||||||
|
return callback(null, tasks, faults, operations, cacheUntil)
|
||||||
|
)
|
||||||
|
|
||||||
|
db.getDueTasks(deviceId, timestamp, (err, dueTasks, nextTimestamp) ->
|
||||||
|
if err
|
||||||
|
callback?(err)
|
||||||
|
return callback = null
|
||||||
|
|
||||||
|
tasks = dueTasks
|
||||||
|
cacheUntil = nextTimestamp or 0
|
||||||
|
|
||||||
|
if faults? and operations?
|
||||||
|
return callback(null, tasks, faults, operations, cacheUntil)
|
||||||
|
)
|
||||||
|
|
||||||
|
db.getOperations(deviceId, (err, _operations) ->
|
||||||
|
if err
|
||||||
|
callback?(err)
|
||||||
|
return callback = null
|
||||||
|
|
||||||
|
operations = _operations
|
||||||
|
|
||||||
|
if faults? and tasks?
|
||||||
|
return callback(null, tasks, faults, operations, cacheUntil)
|
||||||
|
)
|
||||||
|
|
||||||
|
if faults? and tasks? and operations?
|
||||||
|
return callback(null, tasks, faults, operations, cacheUntil)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
cacheDueTasksAndFaultsAndOperations = (deviceId, tasks, faults, operations, cacheUntil, callback) ->
|
||||||
|
v = {}
|
||||||
|
v.tasks = tasks if tasks.length
|
||||||
|
v.faults = faults if Object.keys(faults).length
|
||||||
|
v.operations = operations if Object.keys(operations).length
|
||||||
|
|
||||||
|
if cacheUntil
|
||||||
|
ttl = Math.trunc((Date.now() - cacheUntil) / 1000)
|
||||||
|
else
|
||||||
|
ttl = config.get('MAX_CACHE_TTL', deviceId)
|
||||||
|
|
||||||
|
cache.set("#{deviceId}_tasks_faults_operations", JSON.stringify(v), ttl, callback)
|
||||||
|
|
||||||
|
|
||||||
|
processRequest = (sessionContext, rpc) ->
|
||||||
|
if rpc.cpeRequest?
|
||||||
|
if rpc.cpeRequest.name is 'Inform'
|
||||||
|
logger.accessInfo({
|
||||||
|
sessionContext: sessionContext
|
||||||
|
message: 'Inform'
|
||||||
|
rpc: rpc
|
||||||
|
})
|
||||||
|
inform(sessionContext, rpc)
|
||||||
|
else if rpc.cpeRequest.name is 'TransferComplete'
|
||||||
|
logger.accessInfo({
|
||||||
|
sessionContext: sessionContext
|
||||||
|
message: 'CPE request'
|
||||||
|
rpc: rpc
|
||||||
|
})
|
||||||
|
transferComplete(sessionContext, rpc)
|
||||||
|
else if rpc.cpeRequest.name is 'GetRPCMethods'
|
||||||
|
logger.accessInfo({
|
||||||
|
sessionContext: sessionContext
|
||||||
|
message: 'CPE request'
|
||||||
|
rpc: rpc
|
||||||
|
})
|
||||||
|
res = soap.response({
|
||||||
|
id : rpc.id
|
||||||
|
acsResponse : {name: 'GetRPCMethodsResponse', methodList: ['Inform', 'GetRPCMethods', 'TransferComplete']}
|
||||||
|
cwmpVersion : sessionContext.cwmpVersion
|
||||||
|
})
|
||||||
|
writeResponse(sessionContext, res)
|
||||||
|
else
|
||||||
|
return throwError(new Error('ACS method not supported'), sessionContext.httpResponse)
|
||||||
|
else if rpc.cpeResponse
|
||||||
|
session.rpcResponse(sessionContext, rpc.id, rpc.cpeResponse, (err) ->
|
||||||
|
return throwError(err, sessionContext.httpResponse) if err
|
||||||
|
nextRpc(sessionContext)
|
||||||
|
)
|
||||||
|
else if rpc.cpeFault
|
||||||
|
logger.accessWarn({
|
||||||
|
sessionContext: sessionContext
|
||||||
|
message: 'CPE fault'
|
||||||
|
rpc:rpc
|
||||||
|
})
|
||||||
|
session.rpcFault(sessionContext, rpc.id, rpc.cpeFault, (err, fault) ->
|
||||||
|
return throwError(err, sessionContext.httpResponse) if err
|
||||||
|
|
||||||
|
if fault
|
||||||
|
recordFault(sessionContext, fault)
|
||||||
|
session.clearProvisions(sessionContext)
|
||||||
|
nextRpc(sessionContext)
|
||||||
|
)
|
||||||
|
else # CPE sent empty response
|
||||||
|
session.timeoutOperations(sessionContext, (err, faults, operations) ->
|
||||||
|
return throwError(err, sessionContext.httpResponse) if err
|
||||||
|
|
||||||
|
for fault, i in faults
|
||||||
|
for k, v of operations[i].retries
|
||||||
|
sessionContext.retries[k] = v
|
||||||
|
|
||||||
|
recordFault(sessionContext, fault, operations[i].provisions, operations[i].channels)
|
||||||
|
|
||||||
|
nextRpc(sessionContext)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
listener = (httpRequest, httpResponse) ->
|
||||||
|
stats.totalRequests += 1
|
||||||
|
if httpRequest.method != 'POST'
|
||||||
|
httpResponse.writeHead 405, {'Allow': 'POST', 'Connection': 'close'}
|
||||||
|
httpResponse.end('405 Method Not Allowed')
|
||||||
|
return
|
||||||
|
|
||||||
|
sessionId = null
|
||||||
# Separation by comma is important as some devices don't comform to standard
|
# Separation by comma is important as some devices don't comform to standard
|
||||||
COOKIE_REGEX = /\s*([a-zA-Z0-9\-_]+?)\s*=\s*"?([a-zA-Z0-9\-_]*?)"?\s*(,|;|$)/g
|
COOKIE_REGEX = /\s*([a-zA-Z0-9\-_]+?)\s*=\s*"?([a-zA-Z0-9\-_]*?)"?\s*(,|;|$)/g
|
||||||
while match = COOKIE_REGEX.exec(httpRequest.headers.cookie)
|
while match = COOKIE_REGEX.exec(httpRequest.headers.cookie)
|
||||||
sessionId = match[2] if match[1] == 'session'
|
sessionId = match[2] if match[1] == 'session'
|
||||||
|
|
||||||
return callback() if not sessionId?
|
# If overloaded, ask CPE to retry in 60 seconds
|
||||||
|
if not sessionId and stats.concurrentRequests > MAX_CONCURRENT_REQUESTS
|
||||||
sessionContext = currentSessions.get(httpRequest.connection)?[sessionId]
|
httpResponse.writeHead(503, {'Retry-after': 60, 'Connection': 'close'})
|
||||||
|
httpResponse.end('503 Service Unavailable')
|
||||||
if sessionContext?
|
stats.droppedRequests += 1
|
||||||
return callback(null, sessionContext)
|
|
||||||
|
|
||||||
setTimeout(() ->
|
|
||||||
db.redisClient.eval('local v=redis.call("get",KEYS[1]);redis.call("del",KEYS[1]);return v;', 1, "session_#{sessionId}", (err, sessionContextString) ->
|
|
||||||
return callback(err) if err or not sessionContextString
|
|
||||||
session.deserialize(sessionContextString, (err, sessionContext) ->
|
|
||||||
return callback(err) if err
|
|
||||||
currentSessions.get(httpRequest.connection)[sessionId] = sessionContext
|
|
||||||
callback(null, sessionContext)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
, 100
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
currentSessions = new WeakMap()
|
|
||||||
|
|
||||||
# When socket closes, store active sessions in redis
|
|
||||||
onConnection = (socket) ->
|
|
||||||
currentSessions.set(socket, {})
|
|
||||||
socket.on('close', () ->
|
|
||||||
sessions = currentSessions.get(socket)
|
|
||||||
for sessionId, sessionContext of sessions
|
|
||||||
delete sessionContext.httpRequest
|
|
||||||
delete sessionContext.httpResponse
|
|
||||||
session.serialize(sessionContext, (err, sessionContextString) ->
|
|
||||||
return throwError(err) if err
|
|
||||||
# TODO don't set if process is shutting down
|
|
||||||
db.redisClient.setex("session_#{sessionId}", sessionContext.timeout, sessionContextString, (err) ->
|
|
||||||
return throwError(err) if err
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
listener = (httpRequest, httpResponse) ->
|
|
||||||
if httpRequest.method != 'POST'
|
|
||||||
httpResponse.writeHead 405, {'Allow': 'POST'}
|
|
||||||
httpResponse.end('405 Method Not Allowed')
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if httpRequest.headers['content-encoding']?
|
if httpRequest.headers['content-encoding']?
|
||||||
@@ -577,12 +819,19 @@ listener = (httpRequest, httpResponse) ->
|
|||||||
when 'deflate'
|
when 'deflate'
|
||||||
stream = httpRequest.pipe(zlib.createInflate())
|
stream = httpRequest.pipe(zlib.createInflate())
|
||||||
else
|
else
|
||||||
httpResponse.writeHead(415)
|
httpResponse.writeHead(415, {'Connection': 'close'})
|
||||||
httpResponse.end('415 Unsupported Media Type')
|
httpResponse.end('415 Unsupported Media Type')
|
||||||
return
|
return
|
||||||
else
|
else
|
||||||
stream = httpRequest
|
stream = httpRequest
|
||||||
|
|
||||||
|
stats.concurrentRequests += 1
|
||||||
|
httpRequest.on('aborted', () ->
|
||||||
|
stats.concurrentRequests -= 1
|
||||||
|
# In some cases event end can be emitted after aborted event
|
||||||
|
httpRequest.removeAllListeners('end')
|
||||||
|
)
|
||||||
|
|
||||||
chunks = []
|
chunks = []
|
||||||
bytes = 0
|
bytes = 0
|
||||||
|
|
||||||
@@ -591,126 +840,119 @@ listener = (httpRequest, httpResponse) ->
|
|||||||
bytes += chunk.length
|
bytes += chunk.length
|
||||||
)
|
)
|
||||||
|
|
||||||
httpRequest.getBody = () ->
|
stream.on('end', () ->
|
||||||
# Write all chunks into a Buffer
|
|
||||||
body = new Buffer(bytes)
|
body = new Buffer(bytes)
|
||||||
offset = 0
|
offset = 0
|
||||||
chunks.forEach((chunk) ->
|
chunks.forEach((chunk) ->
|
||||||
chunk.copy(body, offset, 0, chunk.length)
|
chunk.copy(body, offset, 0, chunk.length)
|
||||||
offset += chunk.length
|
offset += chunk.length
|
||||||
)
|
)
|
||||||
return body
|
|
||||||
|
|
||||||
stream.on('end', () ->
|
parsedRpc = (sessionContext, rpc, parseWarnings) ->
|
||||||
rpc = null
|
for w in parseWarnings
|
||||||
getSession(httpRequest, f = (err, sessionContext) ->
|
w.sessionContext = sessionContext
|
||||||
return throwError(err, httpResponse) if err
|
w.rpc = rpc;
|
||||||
rpc ?= soap.request(httpRequest, sessionContext?.cwmpVersion)
|
logger.accessWarn(w)
|
||||||
if not sessionContext?
|
|
||||||
if rpc.cpeRequest?.name isnt 'Inform'
|
|
||||||
httpResponse.writeHead(400)
|
|
||||||
httpResponse.end('Session is expired')
|
|
||||||
return
|
|
||||||
|
|
||||||
deviceId = common.generateDeviceId(rpc.cpeRequest.deviceId)
|
|
||||||
return session.init(deviceId, rpc.cwmpVersion, rpc.sessionTimeout ? config.get('SESSION_TIMEOUT', deviceId), (err, sessionContext) ->
|
|
||||||
return throwError(err, httpResponse) if err
|
|
||||||
|
|
||||||
sessionContext.sessionId = crypto.randomBytes(8).toString('hex')
|
|
||||||
|
|
||||||
currentSessions.get(httpRequest.connection)[sessionContext.sessionId] = sessionContext
|
|
||||||
|
|
||||||
httpRequest.connection.setTimeout(sessionContext.timeout * 1000)
|
|
||||||
|
|
||||||
db.getDueTasksAndFaultsAndOperations(deviceId, sessionContext.timestamp, (err, dueTasks, faults, operations) ->
|
|
||||||
return throwError(err, httpResponse) if err
|
|
||||||
sessionContext.tasks = dueTasks
|
|
||||||
sessionContext.faults = faults
|
|
||||||
sessionContext.retries = {}
|
|
||||||
for k, v of faults
|
|
||||||
sessionContext.retries[k] = v.retries
|
|
||||||
sessionContext.operations = operations
|
|
||||||
|
|
||||||
# Delete expired faults
|
|
||||||
for k, v of sessionContext.faults
|
|
||||||
if v.expiry >= sessionContext.timestamp
|
|
||||||
delete sessionContext.faults[k]
|
|
||||||
sessionContext.faultsTouched ?= {}
|
|
||||||
sessionContext.faultsTouched[k] = true
|
|
||||||
|
|
||||||
f(null, sessionContext)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
sessionContext.httpRequest = httpRequest
|
|
||||||
sessionContext.httpResponse = httpResponse
|
|
||||||
|
|
||||||
if config.get('DEBUG', sessionContext.deviceId)
|
if config.get('DEBUG', sessionContext.deviceId)
|
||||||
dump = "# REQUEST #{new Date(Date.now())}\n" + JSON.stringify(httpRequest.headers) + "\n#{httpRequest.getBody()}\n\n"
|
dump = "# REQUEST #{new Date(Date.now())}\n" + JSON.stringify(httpRequest.headers) + "\n#{body}\n\n"
|
||||||
require('fs').appendFile("./debug/#{sessionContext.deviceId}.dump", dump, (err) ->
|
require('fs').appendFile("./debug/#{sessionContext.deviceId}.dump", dump, (err) ->
|
||||||
return throwError(err) if err
|
return throwError(err) if err
|
||||||
)
|
)
|
||||||
|
|
||||||
if rpc.cpeRequest?
|
processRequest(sessionContext, rpc)
|
||||||
if rpc.cpeRequest.name is 'Inform'
|
|
||||||
logger.accessInfo({
|
getSession(httpRequest.connection, sessionId, (err, sessionContext) ->
|
||||||
sessionContext: sessionContext
|
return throwError(err, httpResponse) if err
|
||||||
message: 'Inform'
|
|
||||||
rpc: rpc
|
if sessionContext
|
||||||
})
|
sessionContext.httpRequest = httpRequest
|
||||||
inform(sessionContext, rpc)
|
sessionContext.httpResponse = httpResponse
|
||||||
else if rpc.cpeRequest.name is 'TransferComplete'
|
|
||||||
logger.accessInfo({
|
if sessionContext.sessionId != sessionId or
|
||||||
sessionContext: sessionContext
|
sessionContext.lastActivity + sessionContext.timeout * 1000 < Date.now()
|
||||||
message: 'CPE request'
|
logger.accessError({message: 'Invalid session', sessionContext: sessionContext})
|
||||||
rpc: rpc
|
httpResponse.writeHead(400, {'Connection': 'close'})
|
||||||
})
|
httpResponse.end('Invalid session')
|
||||||
transferComplete(sessionContext, rpc)
|
stats.concurrentRequests -= 1
|
||||||
else if rpc.cpeRequest.name is 'GetRPCMethods'
|
return
|
||||||
logger.accessInfo({
|
else if stats.concurrentRequests > MAX_CONCURRENT_REQUESTS
|
||||||
sessionContext: sessionContext
|
# Check again just in case device included old session ID from the previous session
|
||||||
message: 'CPE request'
|
httpResponse.writeHead(503, {'Retry-after': 60, 'Connection': 'close'})
|
||||||
rpc: rpc
|
httpResponse.end('503 Service Unavailable')
|
||||||
})
|
stats.droppedRequests += 1
|
||||||
res = soap.response({
|
stats.concurrentRequests -= 1
|
||||||
id : rpc.id
|
return
|
||||||
acsResponse : {name: 'GetRPCMethodsResponse', methodList: ['Inform', 'GetRPCMethods', 'TransferComplete']}
|
|
||||||
cwmpVersion : sessionContext.cwmpVersion
|
parseWarnings = []
|
||||||
})
|
try
|
||||||
writeResponse(sessionContext, res)
|
rpc = soap.request(body, sessionContext?.cwmpVersion, parseWarnings)
|
||||||
else
|
catch err
|
||||||
return throwError(new Error('ACS method not supported'), sessionContext.httpResponse)
|
logger.accessError({
|
||||||
else if rpc.cpeResponse
|
message: 'XML parse error'
|
||||||
session.rpcResponse(sessionContext, rpc.id, rpc.cpeResponse, (err) ->
|
parseError: err.message.trim()
|
||||||
return throwError(err, sessionContext.httpResponse) if err
|
sessionContext: sessionContext or {httpRequest: httpRequest, httpResponse: httpResponse}
|
||||||
nextRpc(sessionContext)
|
|
||||||
)
|
|
||||||
else if rpc.cpeFault
|
|
||||||
logger.accessWarn({
|
|
||||||
sessionContext: sessionContext
|
|
||||||
message: 'CPE fault'
|
|
||||||
rpc:rpc
|
|
||||||
})
|
})
|
||||||
session.rpcFault(sessionContext, rpc.id, rpc.cpeFault, (err, fault) ->
|
httpResponse.writeHead(400, {'Connection': 'close'})
|
||||||
return throwError(err, sessionContext.httpResponse) if err
|
httpResponse.end(err.message)
|
||||||
|
stats.concurrentRequests -= 1
|
||||||
|
return
|
||||||
|
|
||||||
if fault
|
if sessionContext
|
||||||
recordFault(sessionContext, fault)
|
if rpc.cpeRequest?.name is 'Inform' or
|
||||||
session.clearProvisions(sessionContext)
|
(!sessionContext.rpcRequest ^ !(rpc.cpeResponse or rpc.cpeFault))
|
||||||
nextRpc(sessionContext)
|
logger.accessError({
|
||||||
)
|
message: 'Bad session state'
|
||||||
else # CPE sent empty response
|
sessionContext: sessionContext
|
||||||
session.timeoutOperations(sessionContext, (err, faults, operations) ->
|
})
|
||||||
return throwError(err, sessionContext.httpResponse) if err
|
httpResponse.writeHead(400, {'Connection': 'close'})
|
||||||
|
httpResponse.end('Bad session state')
|
||||||
for fault, i in faults
|
stats.concurrentRequests -= 1
|
||||||
for k, v of operations[i].retries
|
return
|
||||||
sessionContext.retries[k] = v
|
|
||||||
|
return parsedRpc(sessionContext, rpc, parseWarnings)
|
||||||
recordFault(sessionContext, fault, operations[i].provisions, operations[i].channels)
|
|
||||||
|
if rpc.cpeRequest?.name isnt 'Inform'
|
||||||
nextRpc(sessionContext)
|
logger.accessError({
|
||||||
|
message: 'Invalid session'
|
||||||
|
sessionContext: sessionContext or {httpRequest: httpRequest, httpResponse: httpResponse}
|
||||||
|
})
|
||||||
|
httpResponse.writeHead(400, {'Connection': 'close'})
|
||||||
|
httpResponse.end('Invalid session')
|
||||||
|
stats.concurrentRequests -= 1
|
||||||
|
return
|
||||||
|
|
||||||
|
stats.initiatedSessions += 1
|
||||||
|
deviceId = common.generateDeviceId(rpc.cpeRequest.deviceId)
|
||||||
|
return session.init(deviceId, rpc.cwmpVersion, rpc.sessionTimeout ? config.get('SESSION_TIMEOUT', deviceId), (err, sessionContext) ->
|
||||||
|
return throwError(err, httpResponse) if err
|
||||||
|
|
||||||
|
sessionContext.httpRequest = httpRequest
|
||||||
|
sessionContext.httpResponse = httpResponse
|
||||||
|
sessionContext.sessionId = crypto.randomBytes(8).toString('hex')
|
||||||
|
httpRequest.connection.setTimeout(sessionContext.timeout * 1000)
|
||||||
|
|
||||||
|
getDueTasksAndFaultsAndOperations(deviceId, sessionContext.timestamp, (err, dueTasks, faults, operations, cacheUntil) ->
|
||||||
|
return throwError(err, httpResponse) if err
|
||||||
|
|
||||||
|
sessionContext.tasks = dueTasks
|
||||||
|
sessionContext.faults = faults
|
||||||
|
sessionContext.retries = {}
|
||||||
|
for k, v of faults
|
||||||
|
sessionContext.retries[k] = v.retries
|
||||||
|
sessionContext.operations = operations
|
||||||
|
sessionContext.cacheUntil = cacheUntil
|
||||||
|
|
||||||
|
# Delete expired faults
|
||||||
|
for k, v of sessionContext.faults
|
||||||
|
if v.expiry >= sessionContext.timestamp
|
||||||
|
delete sessionContext.faults[k]
|
||||||
|
sessionContext.faultsTouched ?= {}
|
||||||
|
sessionContext.faultsTouched[k] = true
|
||||||
|
|
||||||
|
parsedRpc(sessionContext, rpc, parseWarnings)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+184
-195
@@ -19,10 +19,8 @@
|
|||||||
|
|
||||||
config = require './config'
|
config = require './config'
|
||||||
mongodb = require 'mongodb'
|
mongodb = require 'mongodb'
|
||||||
redis = require('redis')
|
|
||||||
common = require './common'
|
common = require './common'
|
||||||
|
|
||||||
redisClient = null
|
|
||||||
tasksCollection = null
|
tasksCollection = null
|
||||||
devicesCollection = null
|
devicesCollection = null
|
||||||
presetsCollection = null
|
presetsCollection = null
|
||||||
@@ -35,7 +33,7 @@ operationsCollection = null
|
|||||||
|
|
||||||
|
|
||||||
connect = (callback) ->
|
connect = (callback) ->
|
||||||
callbackCounter = 10
|
callbackCounter = 9
|
||||||
mongodb.MongoClient.connect(config.get('MONGODB_CONNECTION_URL'), (err, db) ->
|
mongodb.MongoClient.connect(config.get('MONGODB_CONNECTION_URL'), (err, db) ->
|
||||||
return callback(err) if err
|
return callback(err) if err
|
||||||
exports.mongoDb = db
|
exports.mongoDb = db
|
||||||
@@ -105,25 +103,21 @@ connect = (callback) ->
|
|||||||
return callback(err)
|
return callback(err)
|
||||||
)
|
)
|
||||||
|
|
||||||
exports.redisClient = redisClient = redis.createClient(config.get('REDIS_PORT'), config.get('REDIS_HOST'))
|
|
||||||
redisClient.select(config.get('REDIS_DB'), (err) ->
|
|
||||||
if --callbackCounter == 0 or err
|
|
||||||
callbackCounter = 0
|
|
||||||
return callback(err)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
disconnect = () ->
|
disconnect = () ->
|
||||||
exports.mongoDb?.close()
|
exports.mongoDb?.close()
|
||||||
redisClient?.quit()
|
|
||||||
|
|
||||||
|
|
||||||
# Optimize projection by removing overlaps
|
# Optimize projection by removing overlaps
|
||||||
# This modifies given object and returns it
|
# This can modify the object
|
||||||
optimizeProjection = (obj) ->
|
optimizeProjection = (obj) ->
|
||||||
|
if obj['']
|
||||||
|
return {'': obj['']}
|
||||||
|
|
||||||
keys = Object.keys(obj).sort()
|
keys = Object.keys(obj).sort()
|
||||||
return if keys.length <= 1
|
return obj if keys.length <= 1
|
||||||
i = 1
|
i = 1
|
||||||
while i < keys.length
|
while i < keys.length
|
||||||
a = keys[i-1]
|
a = keys[i-1]
|
||||||
@@ -139,21 +133,26 @@ optimizeProjection = (obj) ->
|
|||||||
fetchDevice = (id, timestamp, patterns, callback) ->
|
fetchDevice = (id, timestamp, patterns, callback) ->
|
||||||
MAX_DEPTH = config.get('MAX_DEPTH', id)
|
MAX_DEPTH = config.get('MAX_DEPTH', id)
|
||||||
|
|
||||||
projection = {_id: 1}
|
if not patterns?.length
|
||||||
|
patterns = [[[], (1 << MAX_DEPTH) - 1]]
|
||||||
|
|
||||||
|
projection = {}
|
||||||
projectionTree = {}
|
projectionTree = {}
|
||||||
func = (path, pats, projTree) ->
|
func = (path, pats, projTree) ->
|
||||||
children = {}
|
children = {}
|
||||||
for pat in pats
|
for pat in pats
|
||||||
fragment = pat[0][path.length] or '*'
|
fragment = pat[0][path.length] or '*'
|
||||||
load = 0
|
|
||||||
if fragment == '*'
|
if fragment == '*'
|
||||||
projection[path.join('.')] = 1
|
projection[path.join('.')] = 1 if pat[1] << path.length
|
||||||
return
|
return
|
||||||
|
|
||||||
projTree[fragment] ?= {}
|
projTree[fragment] ?= {}
|
||||||
|
|
||||||
if pat[1] & (1 << path.length)
|
if pat[1] & (1 << path.length)
|
||||||
projection[path.concat('_timestamp').join('.')] = 1
|
projection[path.concat('_timestamp').join('.')] = 1
|
||||||
|
projection[path.concat('_object').join('.')] = 1
|
||||||
|
projection[path.concat('_instance').join('.')] = 1
|
||||||
projection[path.concat([fragment, '_timestamp']).join('.')] = 1
|
projection[path.concat([fragment, '_timestamp']).join('.')] = 1
|
||||||
projection[path.concat([fragment, '_value']).join('.')] = 1
|
projection[path.concat([fragment, '_value']).join('.')] = 1
|
||||||
projection[path.concat([fragment, '_type']).join('.')] = 1
|
projection[path.concat([fragment, '_type']).join('.')] = 1
|
||||||
@@ -175,47 +174,80 @@ fetchDevice = (id, timestamp, patterns, callback) ->
|
|||||||
delete projectionTree['Events']
|
delete projectionTree['Events']
|
||||||
delete projectionTree['Tags']
|
delete projectionTree['Tags']
|
||||||
|
|
||||||
if projection['']?
|
|
||||||
proj = {}
|
|
||||||
else
|
|
||||||
proj = JSON.parse(JSON.stringify(projection))
|
|
||||||
|
|
||||||
for k, v of proj
|
|
||||||
if k == '' or k == 'Events' or k.startsWith('Events.')
|
|
||||||
if k.length < 7 or k.startsWith('Events.Inform.') or k == 'Events.Inform'
|
|
||||||
proj['_lastInform'] = 1
|
|
||||||
if k.length < 7 or k.startsWith('Events.1_BOOT.') or k == 'Events.1_BOOT'
|
|
||||||
proj['_lastBoot'] = 1
|
|
||||||
if k.length < 7 or k.startsWith('Events.0_BOOTSTRAP.') or k == 'Events.0_BOOTSTRAP'
|
|
||||||
proj['_lastBootstrap'] = 1
|
|
||||||
if k.length < 7 or k.startsWith('Events.Registered.') or k == 'Events.Registered'
|
|
||||||
proj['_registered'] = 1
|
|
||||||
delete proj[k] if k != ''
|
|
||||||
|
|
||||||
if k == '' or k == 'DeviceID' or k.startsWith('DeviceID.')
|
|
||||||
if k.length < 9 or k.startsWith('DeviceID.ID.') or k == 'DeviceID.ID'
|
|
||||||
proj['_id'] = 1
|
|
||||||
if k.length < 9 or k.startsWith('DeviceID.Manufacturer.') or k == 'DeviceID.DeManufacturer'
|
|
||||||
proj['_deviceId._Manufacturer'] = 1
|
|
||||||
if k.length < 9 or k.startsWith('DeviceID.ProductClass.') or k == 'DeviceID.ProductClass'
|
|
||||||
proj['_deviceId._ProductClass'] = 1
|
|
||||||
if k.length < 9 or k.startsWith('DeviceID.OUI.') or k == 'DeviceID.OUI'
|
|
||||||
proj['_deviceId._OUI'] = 1
|
|
||||||
if k.length < 9 or k.startsWith('DeviceID.SerialNumber.') or k == 'DeviceID.SerialNumber'
|
|
||||||
proj['_deviceId._SerialNumber'] = 1
|
|
||||||
if k.length < 9
|
|
||||||
proj['_deviceId'] = 1
|
|
||||||
delete proj[k] if k != ''
|
|
||||||
|
|
||||||
if k == '' or k == 'Tags' or k.startsWith('Tags.')
|
|
||||||
proj['_tags'] = 1
|
|
||||||
delete proj[k] if k != ''
|
|
||||||
|
|
||||||
optimizeProjection(proj)
|
|
||||||
|
|
||||||
res = []
|
res = []
|
||||||
loaded = []
|
loaded = []
|
||||||
|
|
||||||
|
projection = optimizeProjection(projection)
|
||||||
|
|
||||||
|
for k, v of projection
|
||||||
|
if k == '' or k == 'Events' or k.startsWith('Events.')
|
||||||
|
if k == '' or k == 'Events' or k == 'Events._writable'
|
||||||
|
res.push([['Events'], timestamp,
|
||||||
|
{object: [timestamp, 1], writable: [timestamp, 0]}])
|
||||||
|
loaded.push([['Events'], 1]) if k == 'Events._writable'
|
||||||
|
if k == 'Events'
|
||||||
|
projection['_lastInform'] = 1
|
||||||
|
projection['_lastBoot'] = 1
|
||||||
|
projection['_lastBootstrap'] = 1
|
||||||
|
projection['_registered'] = 1
|
||||||
|
loaded.push([['Events'], ((1 << MAX_DEPTH) - 1)])
|
||||||
|
else if k == 'Events.Inform._writable' or k == 'Events.Inform'
|
||||||
|
projection['_lastInform'] = 1
|
||||||
|
loaded.push([['Events', 'Inform'], 1 ^ ((1 << MAX_DEPTH) - 1)])
|
||||||
|
else if k == 'Events.1_BOOT._writable' or k == 'Events.1_BOOT'
|
||||||
|
projection['_lastBoot'] = 1
|
||||||
|
loaded.push([['Events', '1_BOOT'], 1 ^ ((1 << MAX_DEPTH) - 1)])
|
||||||
|
else if k == 'Events.0_BOOTSTRAP._writable' or k == 'Events.0_BOOTSTRAP'
|
||||||
|
projection['_lastBootstrap'] = 1
|
||||||
|
loaded.push([['Events', '0_BOOTSTRAP'], 1 ^ ((1 << MAX_DEPTH) - 1)])
|
||||||
|
else if k == 'Events.Registered._writable' or k == 'Events.Registered'
|
||||||
|
projection['_registered'] = 1
|
||||||
|
loaded.push([['Events', 'Registered'], 1 ^ ((1 << MAX_DEPTH) - 1)])
|
||||||
|
else if k.endsWith('._writable') and k != 'Events._writable'
|
||||||
|
loaded.push([k.split('.').slice(0, 2), 1 ^ ((1 << MAX_DEPTH) - 1)])
|
||||||
|
delete projection[k] if k != ''
|
||||||
|
|
||||||
|
if k == '' or k == 'DeviceID' or k.startsWith('DeviceID.')
|
||||||
|
if k == '' or k == 'DeviceID' or k == 'DeviceID._writable'
|
||||||
|
res.push([['DeviceID'], timestamp,
|
||||||
|
{object: [timestamp, 1], writable: [timestamp, 0]}])
|
||||||
|
loaded.push([['DeviceID'], 1]) if k == 'DeviceID._writable'
|
||||||
|
if k == 'DeviceID'
|
||||||
|
projection['_id'] = 1
|
||||||
|
projection['_deviceId'] = 1
|
||||||
|
loaded.push([['DeviceID'], ((1 << MAX_DEPTH) - 1)])
|
||||||
|
else if k == 'DeviceID.ID._writable' or k == 'DeviceID.ID'
|
||||||
|
projection['_id'] = 1
|
||||||
|
loaded.push([['DeviceID', 'ID'], 1 ^ ((1 << MAX_DEPTH) - 1)])
|
||||||
|
else if k == 'DeviceID.Manufacturer._writable' or k == 'DeviceID.DeManufacturer'
|
||||||
|
projection['_deviceId._Manufacturer'] = 1
|
||||||
|
loaded.push([['DeviceID', 'Manufacturer'], 1 ^ ((1 << MAX_DEPTH) - 1)])
|
||||||
|
else if k == 'DeviceID.ProductClass._writable' or k == 'DeviceID.ProductClass'
|
||||||
|
projection['_deviceId._ProductClass'] = 1
|
||||||
|
loaded.push([['DeviceID', 'ProductClass'], 1 ^ ((1 << MAX_DEPTH) - 1)])
|
||||||
|
else if k == 'DeviceID.OUI._writable' or k == 'DeviceID.OUI'
|
||||||
|
projection['_deviceId._OUI'] = 1
|
||||||
|
loaded.push([['DeviceID', 'ProductClass'], 1 ^ ((1 << MAX_DEPTH) - 1)])
|
||||||
|
else if k == 'DeviceID.SerialNumber._writable' or k == 'DeviceID.SerialNumber'
|
||||||
|
projection['_deviceId._SerialNumber'] = 1
|
||||||
|
loaded.push([['DeviceID', 'SerialNumber'], 1 ^ ((1 << MAX_DEPTH) - 1)])
|
||||||
|
else if k.endsWith('._writable') and k != 'DeviceID._writable'
|
||||||
|
loaded.push([k.split('.').slice(0, 2), 1 ^ ((1 << MAX_DEPTH) - 1)])
|
||||||
|
delete projection[k] if k != ''
|
||||||
|
|
||||||
|
if k == 'Tags' or k.startsWith('Tags.')
|
||||||
|
if not projection['_tags']
|
||||||
|
projection['_tags'] = 1
|
||||||
|
loaded.push([['Tags'], ((1 << MAX_DEPTH) - 1)])
|
||||||
|
delete projection[k]
|
||||||
|
|
||||||
|
if projection['']
|
||||||
|
proj = {}
|
||||||
|
else if Object.keys(projection).length == 0
|
||||||
|
proj = {'_id': 1}
|
||||||
|
else
|
||||||
|
proj = projection
|
||||||
|
|
||||||
devicesCollection.findOne({'_id' : id}, proj, (err, device) ->
|
devicesCollection.findOne({'_id' : id}, proj, (err, device) ->
|
||||||
return callback(err) if err or not device?
|
return callback(err) if err or not device?
|
||||||
|
|
||||||
@@ -225,18 +257,27 @@ fetchDevice = (id, timestamp, patterns, callback) ->
|
|||||||
else
|
else
|
||||||
if projection[path.join('.')]
|
if projection[path.join('.')]
|
||||||
descendantsFetched = true
|
descendantsFetched = true
|
||||||
loaded.push([path, ((1 << path.length) - 1) ^ ((1 << MAX_DEPTH) - 1)])
|
if path.length and projection[path.slice(0, -1).concat("_timestamp").join('.')]
|
||||||
if projection[path.concat('_writable').join('.')]
|
thisFetched = true
|
||||||
|
loaded.push([path, ((1 << (path.length - 1)) - 1) ^ ((1 << MAX_DEPTH) - 1)])
|
||||||
|
else
|
||||||
|
loaded.push([path, ((1 << path.length) - 1) ^ ((1 << MAX_DEPTH) - 1)])
|
||||||
|
else if projection[path.concat('_writable').join('.')]
|
||||||
loaded.push([path, 1 << (path.length - 1)])
|
loaded.push([path, 1 << (path.length - 1)])
|
||||||
thisFetched = true
|
thisFetched = true
|
||||||
|
|
||||||
if obj['_timestamp']?
|
if obj['_timestamp']?
|
||||||
obj['_timestamp'] = +obj['_timestamp']
|
obj['_timestamp'] = +obj['_timestamp']
|
||||||
|
|
||||||
|
# For compatibility with v1.0 database
|
||||||
|
if obj['_instance'] and not obj['_object']?
|
||||||
|
obj['_object'] = true
|
||||||
|
|
||||||
if thisFetched
|
if thisFetched
|
||||||
attrs = {}
|
attrs = {}
|
||||||
|
|
||||||
t = if obj['_timestamp'] > timestamp then obj['_timestamp'] else timestamp
|
t = obj['_timestamp'] or 1
|
||||||
|
t = timestamp if timestamp > t
|
||||||
|
|
||||||
if obj['_value']?
|
if obj['_value']?
|
||||||
attrs.value = [obj['_timestamp'] or 1, [obj['_value'], obj['_type']]]
|
attrs.value = [obj['_timestamp'] or 1, [obj['_value'], obj['_type']]]
|
||||||
@@ -244,73 +285,63 @@ fetchDevice = (id, timestamp, patterns, callback) ->
|
|||||||
obj['_object'] = false
|
obj['_object'] = false
|
||||||
|
|
||||||
if obj['_writable']?
|
if obj['_writable']?
|
||||||
attrs.writable = [timestamp, if obj['_writable'] then 1 else 0]
|
attrs.writable = [timestamp or 1, if obj['_writable'] then 1 else 0]
|
||||||
|
|
||||||
if obj['_object']? or obj['_instance']?
|
if obj['_object']?
|
||||||
attrs.object = [t, if (obj['_object'] or obj['_instance']) then 1 else 0]
|
attrs.object = [t, if obj['_object'] then 1 else 0]
|
||||||
|
|
||||||
res.push([path, t, attrs])
|
res.push([path, t, attrs])
|
||||||
|
|
||||||
for k, v of obj
|
for k, v of obj
|
||||||
if k[0] != '_'
|
if not k.startsWith('_')
|
||||||
kk = k
|
kk = k
|
||||||
obj['_object'] = true
|
obj['_object'] = true
|
||||||
storeParams(v, path.concat(k), obj['_timestamp'] or 1, descendantsFetched, projTree?[k])
|
storeParams(v, path.concat(k), obj['_timestamp'], descendantsFetched, projTree?[k])
|
||||||
delete projTree[kk] if projTree
|
delete projTree[kk] if projTree
|
||||||
|
|
||||||
if obj['_object'] or obj['_instance']
|
if not descendantsFetched
|
||||||
if descendantsFetched
|
for k, v of projTree
|
||||||
res.push([path.concat('*'), obj['_timestamp']]) if obj['_timestamp']
|
p = path.concat(k)
|
||||||
else
|
loaded.push([p, ((1 << path.length) - 1) ^ ((1 << MAX_DEPTH) - 1)])
|
||||||
for k, v of projTree
|
if (obj['_object'] or path.length == 0) and obj['_timestamp']
|
||||||
p = path.concat(k)
|
res.push([p, obj['_timestamp']])
|
||||||
res.push([p, +(obj['_timestamp'] ? 1)])
|
else if (obj['_object'] or path.length == 0) and obj['_timestamp']
|
||||||
loaded.push([p, ((1 << path.length) - 1) ^ ((1 << MAX_DEPTH) - 1)])
|
res.push([path.concat('*'), obj['_timestamp']])
|
||||||
|
|
||||||
for k, v of device
|
for k, v of device
|
||||||
switch k
|
switch k
|
||||||
when '_lastInform'
|
when '_lastInform'
|
||||||
res.push([['Events'], timestamp,
|
|
||||||
{object: [timestamp, 1], writable: [timestamp, 0]}])
|
|
||||||
res.push([['Events', 'Inform'], +v,
|
res.push([['Events', 'Inform'], +v,
|
||||||
{object: [+v, 0], writable: [+v, 0], value: [+v, [+v, 'xsd:dateTime']]}])
|
{object: [+v, 0], writable: [+v, 0], value: [+v, [+v, 'xsd:dateTime']]}])
|
||||||
delete device[k]
|
delete device[k]
|
||||||
when '_lastBoot'
|
when '_lastBoot'
|
||||||
res.push([['Events'], timestamp,
|
|
||||||
{object: [timestamp, 1], writable: [timestamp, 0]}])
|
|
||||||
res.push([['Events', '1_BOOT'], +v,
|
res.push([['Events', '1_BOOT'], +v,
|
||||||
{object: [+v, 0], writable: [+v, 0], value: [+v, [+v, 'xsd:dateTime']]}])
|
{object: [+v, 0], writable: [+v, 0], value: [+v, [+v, 'xsd:dateTime']]}])
|
||||||
delete device[k]
|
delete device[k]
|
||||||
when '_lastBootstrap'
|
when '_lastBootstrap'
|
||||||
res.push([['Events'], timestamp,
|
|
||||||
{object: [timestamp, 1], writable: [timestamp, 0]}])
|
|
||||||
res.push([['Events', '0_BOOTSTRAP'], +v,
|
res.push([['Events', '0_BOOTSTRAP'], +v,
|
||||||
{object: [+v, 0], writable: [+v, 0], value: [+v, [+v, 'xsd:dateTime']]}])
|
{object: [+v, 0], writable: [+v, 0], value: [+v, [+v, 'xsd:dateTime']]}])
|
||||||
delete device[k]
|
delete device[k]
|
||||||
when '_registered'
|
when '_registered'
|
||||||
res.push([['Events'], timestamp,
|
|
||||||
{object: [timestamp, 1], writable: [timestamp, 0]}])
|
|
||||||
# Use current timestamp for registered event attribute timestamps
|
# Use current timestamp for registered event attribute timestamps
|
||||||
res.push([['Events', 'Registered'], timestamp,
|
res.push([['Events', 'Registered'], timestamp,
|
||||||
{object: [timestamp, 0], writable: [timestamp, 0], value: [timestamp, [v, 'xsd:dateTime']]}])
|
{object: [timestamp, 0], writable: [timestamp, 0], value: [timestamp, [+v, 'xsd:dateTime']]}])
|
||||||
delete device[k]
|
delete device[k]
|
||||||
when '_id'
|
when '_id'
|
||||||
res.push([['DeviceID'], timestamp,
|
if projection[''] or projection['_id']
|
||||||
{object: [timestamp, 1], writable: [timestamp, 0]}])
|
res.push([['DeviceID', 'ID'], timestamp,
|
||||||
res.push([['DeviceID', 'ID'], timestamp,
|
{object: [timestamp, 0], writable: [timestamp, 0], value: [timestamp, [v, 'xsd:string']]}])
|
||||||
{object: [timestamp, 0], writable: [timestamp, 0], value: [timestamp, [v, 'xsd:string']]}])
|
|
||||||
delete device[k]
|
delete device[k]
|
||||||
when '_tags'
|
when '_tags'
|
||||||
res.push([['Tags'], timestamp,
|
if v.length
|
||||||
{object: [timestamp, 1], writable: [timestamp, 0]}])
|
res.push([['Tags'], timestamp,
|
||||||
|
{object: [timestamp, 1], writable: [timestamp, 0]}])
|
||||||
for t in v
|
for t in v
|
||||||
t = t.replace(/[^a-zA-Z0-9\-]+/g, '_')
|
t = t.replace(/[^a-zA-Z0-9\-]+/g, '_')
|
||||||
res.push([['Tags', t], timestamp,
|
res.push([['Tags', t], timestamp,
|
||||||
{object: [timestamp, 0], writable: [timestamp, 1], value: [timestamp, [true, 'xsd:boolean']]}])
|
{object: [timestamp, 0], writable: [timestamp, 1], value: [timestamp, [true, 'xsd:boolean']]}])
|
||||||
delete device[k]
|
delete device[k]
|
||||||
when '_deviceId'
|
when '_deviceId'
|
||||||
res.push([['DeviceID'], timestamp,
|
|
||||||
{object: [timestamp, 1], writable: [timestamp, 0]}])
|
|
||||||
if v['_Manufacturer']?
|
if v['_Manufacturer']?
|
||||||
res.push([['DeviceID', 'Manufacturer'], timestamp,
|
res.push([['DeviceID', 'Manufacturer'], timestamp,
|
||||||
{object: [timestamp, 0], writable: [timestamp, 0], value: [timestamp, [v['_Manufacturer'], 'xsd:string']]}])
|
{object: [timestamp, 0], writable: [timestamp, 0], value: [timestamp, [v['_Manufacturer'], 'xsd:string']]}])
|
||||||
@@ -331,7 +362,7 @@ fetchDevice = (id, timestamp, patterns, callback) ->
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
saveDevice = (deviceId, deviceData, isNew, callback) ->
|
saveDevice = (deviceId, deviceData, isNew, sessionTimestamp, callback) ->
|
||||||
update = {'$set' : {}, '$unset' : {}, '$addToSet' : {}, '$pull' : {}}
|
update = {'$set' : {}, '$unset' : {}, '$addToSet' : {}, '$pull' : {}}
|
||||||
|
|
||||||
iter = deviceData.timestamps.diff()
|
iter = deviceData.timestamps.diff()
|
||||||
@@ -339,6 +370,10 @@ saveDevice = (deviceId, deviceData, isNew, callback) ->
|
|||||||
continue if diff[0].wildcard != (1 << (diff[0].length - 1))
|
continue if diff[0].wildcard != (1 << (diff[0].length - 1))
|
||||||
continue if diff[0][0] in ['Events', 'DeviceID', 'Tags']
|
continue if diff[0][0] in ['Events', 'DeviceID', 'Tags']
|
||||||
|
|
||||||
|
# Param timestamps may be greater than session timestamp to track revisions
|
||||||
|
if diff[2] > sessionTimestamp
|
||||||
|
diff[2] = sessionTimestamp
|
||||||
|
|
||||||
if not diff[2]? and diff[1]?
|
if not diff[2]? and diff[1]?
|
||||||
update['$unset'][diff[0].slice(0, -1).concat('_timestamp').join('.')] = 1
|
update['$unset'][diff[0].slice(0, -1).concat('_timestamp').join('.')] = 1
|
||||||
else if diff[2] != diff[1]
|
else if diff[2] != diff[1]
|
||||||
@@ -403,17 +438,21 @@ saveDevice = (deviceId, deviceData, isNew, callback) ->
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
for attrName of diff[2]
|
for attrName of diff[2]
|
||||||
if diff[2][attrName][1]? or diff[1]?[attrName]?[1]?
|
# Param timestamps may be greater than session timestamp to track revisions
|
||||||
|
if diff[2][attrName][0] > sessionTimestamp
|
||||||
|
diff[2][attrName][0] = sessionTimestamp
|
||||||
|
|
||||||
|
if diff[2][attrName][1]?
|
||||||
switch attrName
|
switch attrName
|
||||||
when 'value'
|
when 'value'
|
||||||
if not diff[1] or diff[2].value[1][0] != diff[1].value?[1][0]
|
if diff[2].value[1][0] != diff[1]?.value?[1]?[0]
|
||||||
if diff[2].value[1][1] == 'xsd:dateTime'
|
if diff[2].value[1][1] == 'xsd:dateTime' and Number.isInteger(diff[2].value[1][0])
|
||||||
update['$set'][path.concat('_value').join('.')] = new Date(diff[2].value[1][0])
|
update['$set'][path.concat('_value').join('.')] = new Date(diff[2].value[1][0])
|
||||||
else
|
else
|
||||||
update['$set'][path.concat('_value').join('.')] = diff[2].value[1][0]
|
update['$set'][path.concat('_value').join('.')] = diff[2].value[1][0]
|
||||||
if not diff[1] or diff[2].value[1][1] != diff[1].value?[1][1]
|
if diff[2].value[1][1] != diff[1]?.value?[1]?[1]
|
||||||
update['$set'][path.concat('_type').join('.')] = diff[2].value[1][1]
|
update['$set'][path.concat('_type').join('.')] = diff[2].value[1][1]
|
||||||
if not diff[1] or diff[2].value[0] != diff[1].value?[0]
|
if diff[2].value[0] != diff[1]?.value?[0]
|
||||||
update['$set'][path.concat('_timestamp').join('.')] = new Date(diff[2].value[0])
|
update['$set'][path.concat('_timestamp').join('.')] = new Date(diff[2].value[0])
|
||||||
when 'object'
|
when 'object'
|
||||||
if not diff[1]?.object or diff[2].object[1] != diff[1].object?[1]
|
if not diff[1]?.object or diff[2].object[1] != diff[1].object?[1]
|
||||||
@@ -424,12 +463,21 @@ saveDevice = (deviceId, deviceData, isNew, callback) ->
|
|||||||
|
|
||||||
if diff[1]
|
if diff[1]
|
||||||
for attrName of diff[1]
|
for attrName of diff[1]
|
||||||
if attrName not of diff[2]
|
if diff[1][attrName][1]? and not diff[2]?[attrName]?[1]?
|
||||||
update['$unset'][path.concat("_#{attrName}").join('.')] = 1
|
update['$unset'][path.concat("_#{attrName}").join('.')] = 1
|
||||||
if attrName is 'value'
|
if attrName is 'value'
|
||||||
update['$unset'][path.concat('_type').join('.')] = 1
|
update['$unset'][path.concat('_type').join('.')] = 1
|
||||||
update['$unset'][path.concat('_timestamp').join('.')] = 1
|
update['$unset'][path.concat('_timestamp').join('.')] = 1
|
||||||
|
|
||||||
|
update['$unset'] = optimizeProjection(update['$unset'])
|
||||||
|
|
||||||
|
# Remove overlap possibly caused by parameters changing from objects
|
||||||
|
# to regular parameters or vice versa. Reason being that _timestamp
|
||||||
|
# represents two different things depending on whether the parameter
|
||||||
|
# is an object or not.
|
||||||
|
for k of update['$unset']
|
||||||
|
delete update['$unset'][k] if update['$set'][k]?
|
||||||
|
|
||||||
# Remove empty keys
|
# Remove empty keys
|
||||||
for k of update
|
for k of update
|
||||||
if k == '$addToSet'
|
if k == '$addToSet'
|
||||||
@@ -442,82 +490,22 @@ saveDevice = (deviceId, deviceData, isNew, callback) ->
|
|||||||
|
|
||||||
return callback() if Object.keys(update).length == 0
|
return callback() if Object.keys(update).length == 0
|
||||||
|
|
||||||
if (update['$unset']?)
|
if update['$addToSet'] and update['$pull']
|
||||||
optimizeProjection(update['$unset'])
|
# Mongo doesn't allow $addToSet and $pull at the same time
|
||||||
|
update2 = {'$pull': update['$pull']}
|
||||||
|
delete update['$pull']
|
||||||
|
|
||||||
devicesCollection.update({'_id' : deviceId}, update, {upsert: isNew}, (err, result) ->
|
devicesCollection.update({'_id' : deviceId}, update, {upsert: isNew}, (err, result) ->
|
||||||
if not err and result.result.n != 1
|
if not err and result.result.n != 1
|
||||||
return callback(new Error("Device #{deviceId} not found in database"))
|
return callback(new Error("Device #{deviceId} not found in database"))
|
||||||
|
|
||||||
|
if update2
|
||||||
|
return devicesCollection.update({'_id': deviceId}, update2, callback)
|
||||||
|
|
||||||
return callback(err)
|
return callback(err)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
getDueTasksAndFaultsAndOperations = (deviceId, timestamp, callback) ->
|
|
||||||
redisClient.mget("#{deviceId}_tasks", "#{deviceId}_faults", "#{deviceId}_operations", (err, res) ->
|
|
||||||
return callback(err) if err
|
|
||||||
|
|
||||||
if res[0]
|
|
||||||
tasks = JSON.parse(res[0])
|
|
||||||
|
|
||||||
if res[1]?
|
|
||||||
faults = JSON.parse(res[1])
|
|
||||||
|
|
||||||
if res[2]?
|
|
||||||
operations = JSON.parse(res[2])
|
|
||||||
|
|
||||||
if tasks? and faults? and operations?
|
|
||||||
return callback(null, tasks, faults, operations)
|
|
||||||
|
|
||||||
MAX_CACHE_TTL = config.get('MAX_CACHE_TTL', deviceId)
|
|
||||||
|
|
||||||
if not faults?
|
|
||||||
getFaults(deviceId, (err, flts) ->
|
|
||||||
if err
|
|
||||||
callback?(err)
|
|
||||||
return callback = null
|
|
||||||
|
|
||||||
faults = flts
|
|
||||||
if tasks? and operations?
|
|
||||||
return callback(null, tasks, faults, operations)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not tasks?
|
|
||||||
getDueTasks(deviceId, timestamp, (err, dueTasks, nextTimestamp) ->
|
|
||||||
if err
|
|
||||||
callback?(err)
|
|
||||||
return callback = null
|
|
||||||
|
|
||||||
if nextTimestamp?
|
|
||||||
exp = Math.min(0, Math.trunc((nextTimestamp - Date.now()) / 1000))
|
|
||||||
else
|
|
||||||
exp = MAX_CACHE_TTL
|
|
||||||
|
|
||||||
redisClient.setex("#{deviceId}_tasks", exp, JSON.stringify(dueTasks), (err) ->
|
|
||||||
if err
|
|
||||||
callback?(err)
|
|
||||||
return callback = null
|
|
||||||
|
|
||||||
tasks = dueTasks
|
|
||||||
if faults? and operations?
|
|
||||||
return callback(null, tasks, faults, operations)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not operations?
|
|
||||||
getOperations(deviceId, (err, _operations) ->
|
|
||||||
if err
|
|
||||||
callback?(err)
|
|
||||||
return callback = null
|
|
||||||
|
|
||||||
operations = _operations
|
|
||||||
if tasks? and faults?
|
|
||||||
return callback(null, tasks, faults, operations)
|
|
||||||
)
|
|
||||||
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
getFaults = (deviceId, callback) ->
|
getFaults = (deviceId, callback) ->
|
||||||
faultsCollection.find({'_id' : {'$regex' : "^#{common.escapeRegExp(deviceId)}\\:"}}).toArray((err, res) ->
|
faultsCollection.find({'_id' : {'$regex' : "^#{common.escapeRegExp(deviceId)}\\:"}}).toArray((err, res) ->
|
||||||
return callback(err) if err
|
return callback(err) if err
|
||||||
@@ -532,32 +520,22 @@ getFaults = (deviceId, callback) ->
|
|||||||
r.provisions = JSON.parse(r.provisions)
|
r.provisions = JSON.parse(r.provisions)
|
||||||
faults[channel] = r
|
faults[channel] = r
|
||||||
|
|
||||||
MAX_CACHE_TTL = config.get('MAX_CACHE_TTL', deviceId)
|
callback(err, faults)
|
||||||
|
|
||||||
redisClient.setex("#{deviceId}_faults", MAX_CACHE_TTL, JSON.stringify(faults), (err) ->
|
|
||||||
callback(err, faults)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
saveFault = (deviceId, channel, fault, callback) ->
|
saveFault = (deviceId, channel, fault, callback) ->
|
||||||
|
fault = Object.assign({}, fault)
|
||||||
fault._id = "#{deviceId}:#{channel}"
|
fault._id = "#{deviceId}:#{channel}"
|
||||||
fault.device = deviceId
|
fault.device = deviceId
|
||||||
fault.channel = channel
|
fault.channel = channel
|
||||||
fault.timestamp = new Date(fault.timestamp)
|
fault.timestamp = new Date(fault.timestamp)
|
||||||
fault.provisions = JSON.stringify(fault.provisions)
|
fault.provisions = JSON.stringify(fault.provisions)
|
||||||
faultsCollection.save(fault, (err) ->
|
faultsCollection.save(fault, callback)
|
||||||
return callback(err) if err
|
|
||||||
redisClient.del("#{deviceId}_faults", callback)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
deleteFault = (deviceId, channel, callback) ->
|
deleteFault = (deviceId, channel, callback) ->
|
||||||
faultsCollection.remove({_id: "#{deviceId}:#{channel}"}, (err) ->
|
faultsCollection.remove({_id: "#{deviceId}:#{channel}"}, callback)
|
||||||
return callback(err) if err
|
|
||||||
redisClient.del("#{deviceId}_faults", callback)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
getDueTasks = (deviceId, timestamp, callback) ->
|
getDueTasks = (deviceId, timestamp, callback) ->
|
||||||
@@ -600,13 +578,7 @@ getDueTasks = (deviceId, timestamp, callback) ->
|
|||||||
|
|
||||||
|
|
||||||
clearTasks = (deviceId, taskIds, callback) ->
|
clearTasks = (deviceId, taskIds, callback) ->
|
||||||
if not taskIds?.length
|
tasksCollection.remove({'_id' : {'$in' : (new mongodb.ObjectID(id) for id in taskIds)}}, callback)
|
||||||
return callback()
|
|
||||||
|
|
||||||
tasksCollection.remove({'_id' : {'$in' : (new mongodb.ObjectID(id) for id in taskIds)}}, (err) ->
|
|
||||||
return callback(err) if err
|
|
||||||
redisClient.del("#{deviceId}_tasks", callback)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
getOperations = (deviceId, callback) ->
|
getOperations = (deviceId, callback) ->
|
||||||
@@ -623,41 +595,58 @@ getOperations = (deviceId, callback) ->
|
|||||||
r.retries = JSON.parse(r.retries)
|
r.retries = JSON.parse(r.retries)
|
||||||
operations[commandKey] = r
|
operations[commandKey] = r
|
||||||
|
|
||||||
MAX_CACHE_TTL = config.get('MAX_CACHE_TTL', deviceId)
|
callback(err, operations)
|
||||||
|
|
||||||
redisClient.setex("#{deviceId}_operations", MAX_CACHE_TTL, JSON.stringify(operations), (err) ->
|
|
||||||
callback(err, operations)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
saveOperation = (deviceId, commandKey, operation, callback) ->
|
saveOperation = (deviceId, commandKey, operation, callback) ->
|
||||||
|
operation = Object.assign({}, operation)
|
||||||
operation._id = "#{deviceId}:#{commandKey}"
|
operation._id = "#{deviceId}:#{commandKey}"
|
||||||
operation.timestamp = new Date(operation.timestamp)
|
operation.timestamp = new Date(operation.timestamp)
|
||||||
operation.provisions = JSON.stringify(operation.provisions)
|
operation.provisions = JSON.stringify(operation.provisions)
|
||||||
operation.retries = JSON.stringify(operation.retries)
|
operation.retries = JSON.stringify(operation.retries)
|
||||||
operation.args = JSON.stringify(operation.args)
|
operation.args = JSON.stringify(operation.args)
|
||||||
operationsCollection.save(operation, (err) ->
|
operationsCollection.save(operation, callback)
|
||||||
return callback(err) if err
|
|
||||||
redisClient.del("#{deviceId}_operations", callback)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
deleteOperation = (deviceId, commandKey, callback) ->
|
deleteOperation = (deviceId, commandKey, callback) ->
|
||||||
operationsCollection.remove({_id: "#{deviceId}:#{commandKey}"}, (err) ->
|
operationsCollection.remove({_id: "#{deviceId}:#{commandKey}"}, callback)
|
||||||
return callback(err) if err
|
|
||||||
redisClient.del("#{deviceId}_operations", callback)
|
|
||||||
)
|
getPresets = (callback) ->
|
||||||
|
presetsCollection.find().toArray(callback)
|
||||||
|
|
||||||
|
|
||||||
|
getObjects = (callback) ->
|
||||||
|
objectsCollection.find().toArray(callback)
|
||||||
|
|
||||||
|
|
||||||
|
getProvisions = (callback) ->
|
||||||
|
provisionsCollection.find().toArray(callback)
|
||||||
|
|
||||||
|
|
||||||
|
getVirtualParameters = (callback) ->
|
||||||
|
virtualParametersCollection.find().toArray(callback)
|
||||||
|
|
||||||
|
|
||||||
|
getFiles = (callback) ->
|
||||||
|
filesCollection.find().toArray(callback)
|
||||||
|
|
||||||
|
|
||||||
exports.connect = connect
|
exports.connect = connect
|
||||||
exports.disconnect = disconnect
|
exports.disconnect = disconnect
|
||||||
exports.fetchDevice = fetchDevice
|
exports.fetchDevice = fetchDevice
|
||||||
exports.saveDevice = saveDevice
|
exports.saveDevice = saveDevice
|
||||||
exports.getDueTasksAndFaultsAndOperations = getDueTasksAndFaultsAndOperations
|
|
||||||
exports.getFaults = getFaults
|
exports.getFaults = getFaults
|
||||||
exports.saveFault = saveFault
|
exports.saveFault = saveFault
|
||||||
exports.deleteFault = deleteFault
|
exports.deleteFault = deleteFault
|
||||||
exports.clearTasks = clearTasks
|
exports.clearTasks = clearTasks
|
||||||
exports.saveOperation = saveOperation
|
exports.saveOperation = saveOperation
|
||||||
exports.deleteOperation = deleteOperation
|
exports.deleteOperation = deleteOperation
|
||||||
|
exports.getPresets = getPresets
|
||||||
|
exports.getObjects = getObjects
|
||||||
|
exports.getProvisions = getProvisions
|
||||||
|
exports.getVirtualParameters = getVirtualParameters
|
||||||
|
exports.getFiles = getFiles
|
||||||
|
exports.getDueTasks = getDueTasks
|
||||||
|
exports.getOperations = getOperations
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
const common = require("./common");
|
const common = require("./common");
|
||||||
const device = require("./device");
|
const device = require("./device");
|
||||||
|
const scheduling = require("./scheduling");
|
||||||
|
|
||||||
const MAX_DEPTH = config.get("MAX_DEPTH");
|
const MAX_DEPTH = config.get("MAX_DEPTH");
|
||||||
|
|
||||||
@@ -31,8 +32,8 @@ function refresh(sessionContext, provision, declarations, startRevision, endRevi
|
|||||||
path.length = MAX_DEPTH;
|
path.length = MAX_DEPTH;
|
||||||
path.fill("*", l);
|
path.fill("*", l);
|
||||||
let every = 1000 * (provision[2] || 1);
|
let every = 1000 * (provision[2] || 1);
|
||||||
let offset = Math.abs(common.djb2(sessionContext.deviceId)) % 86400000;
|
let offset = scheduling.variance(sessionContext.deviceId, every);
|
||||||
let t = Math.trunc((sessionContext.timestamp + offset) / every) * every - offset;
|
let t = scheduling.interval(sessionContext.timestamp, every, offset);
|
||||||
|
|
||||||
for (let i = l; i < path.length; ++ i)
|
for (let i = l; i < path.length; ++ i)
|
||||||
declarations.push([path.slice(0, i), t, {object: 1, writable: 1, value: t}]);
|
declarations.push([path.slice(0, i), t, {object: 1, writable: 1, value: t}]);
|
||||||
@@ -72,8 +73,8 @@ function reset(sessionContext, provision, declarations, startRevision, endRevisi
|
|||||||
|
|
||||||
|
|
||||||
function download(sessionContext, provision, declarations, startRevision, endRevision) {
|
function download(sessionContext, provision, declarations, startRevision, endRevision) {
|
||||||
let alias = ["FileType", provision[1] || "", "FileName", provision[2] || "",
|
let alias = [["FileType"], provision[1] || "", ["FileName"], provision[2] || "",
|
||||||
"TargetFileName", provision[3] || ""];
|
["TargetFileName"], provision[3] || ""];
|
||||||
|
|
||||||
declarations.push([["Downloads", alias], 1, {}, 1]);
|
declarations.push([["Downloads", alias], 1, {}, 1]);
|
||||||
declarations.push([["Downloads", alias, "Download"], 1, {value: 1}, null,
|
declarations.push([["Downloads", alias, "Download"], 1, {value: 1}, null,
|
||||||
|
|||||||
+115
-102
@@ -22,18 +22,32 @@ const common = require('./common');
|
|||||||
|
|
||||||
|
|
||||||
const CHANGE_FLAGS = {
|
const CHANGE_FLAGS = {
|
||||||
object: 4,
|
object: 2,
|
||||||
writable: 16,
|
writable: 4,
|
||||||
value: 64
|
value: 8
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function parseBool(v) {
|
||||||
|
v = "" + v;
|
||||||
|
if (v === "true" || v === "TRUE" || v === "True" || v === "1")
|
||||||
|
return true;
|
||||||
|
else if (v === "false" || v === "FALSE" || v === "False" || v === "0")
|
||||||
|
return false;
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function sanitizeParameterValue(parameterValue) {
|
function sanitizeParameterValue(parameterValue) {
|
||||||
if (parameterValue[0] != null) {
|
if (parameterValue[0] != null) {
|
||||||
switch (parameterValue[1]) {
|
switch (parameterValue[1]) {
|
||||||
case 'xsd:boolean':
|
case 'xsd:boolean':
|
||||||
if (typeof parameterValue[0] !== 'boolean') {
|
if (typeof parameterValue[0] !== 'boolean') {
|
||||||
parameterValue[0] = !!JSON.parse(parameterValue[0]);
|
let v = parseBool(parameterValue[0]);
|
||||||
|
if (v == null)
|
||||||
|
v = '' + parameterValue[0];
|
||||||
|
parameterValue[0] = v;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'xsd:string':
|
case 'xsd:string':
|
||||||
@@ -44,17 +58,22 @@ function sanitizeParameterValue(parameterValue) {
|
|||||||
case 'xsd:int':
|
case 'xsd:int':
|
||||||
case 'xsd:unsignedInt':
|
case 'xsd:unsignedInt':
|
||||||
if (typeof parameterValue[0] !== 'number') {
|
if (typeof parameterValue[0] !== 'number') {
|
||||||
parameterValue[0] = +parameterValue[0];
|
let v = parseInt(parameterValue[0]);
|
||||||
|
if (isNaN(v))
|
||||||
|
v = '' + parameterValue[0];
|
||||||
|
parameterValue[0] = v;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'xsd:dateTime':
|
case 'xsd:dateTime':
|
||||||
if (typeof parameterValue[0] !== 'number') {
|
if (typeof parameterValue[0] !== 'number') {
|
||||||
if (parameterValue[0].getTime() != null)
|
// Don't use parseInt because it reads date string as a number
|
||||||
parameterValue[0] = parameterValue[0].getTime();
|
let v = +parameterValue[0];
|
||||||
else if (isNaN(parameterValue[0]))
|
if (isNaN(v)) {
|
||||||
parameterValue[0] = Date.parse(parameterValue[0]);
|
v = Date.parse(parameterValue[0]);
|
||||||
else
|
if (isNaN(v))
|
||||||
parameterValue[0] = +parameterValue[0];
|
v = '' + parameterValue[0];
|
||||||
|
}
|
||||||
|
parameterValue[0] = v;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -62,6 +81,7 @@ function sanitizeParameterValue(parameterValue) {
|
|||||||
throw new Error(`Parameter value type "${parameterValue[1]}" not recognized.`);
|
throw new Error(`Parameter value type "${parameterValue[1]}" not recognized.`);
|
||||||
|
|
||||||
parameterValue[0] = JSON.parse(JSON.stringify(parameterValue[0]));
|
parameterValue[0] = JSON.parse(JSON.stringify(parameterValue[0]));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return parameterValue;
|
return parameterValue;
|
||||||
@@ -170,36 +190,59 @@ function unpack(deviceData, path, revision) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function clear(deviceData, path, timestamp, attributes) {
|
function clear(deviceData, path, timestamp, attributes, changeFlags = 0) {
|
||||||
for (let p of deviceData.paths.find(path, true, true, timestamp ? 99 : path.length)) {
|
let changeTrackers = {};
|
||||||
for (let k in deviceData.trackers.get(p))
|
|
||||||
deviceData.changes.add(k);
|
|
||||||
|
|
||||||
let t = deviceData.timestamps.get(p);
|
timestamp = timestamp || 0;
|
||||||
if (!t)
|
|
||||||
|
let descendantsTimestamp = timestamp;
|
||||||
|
if (attributes && attributes.object) {
|
||||||
|
if (attributes.object > descendantsTimestamp)
|
||||||
|
descendantsTimestamp = attributes.object;
|
||||||
|
if (!(attributes.object <= attributes.value))
|
||||||
|
attributes.value = attributes.object;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let p of deviceData.paths.find(path, true, true, descendantsTimestamp ? 99 : path.length)) {
|
||||||
|
let tracker = deviceData.trackers.get(p);
|
||||||
|
for (let k in tracker)
|
||||||
|
changeTrackers[k] |= tracker[k];
|
||||||
|
|
||||||
|
let currentTimestamp = deviceData.timestamps.get(p);
|
||||||
|
if (currentTimestamp === undefined)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (!(timestamp <= t)) {
|
if (timestamp > currentTimestamp ||
|
||||||
|
(descendantsTimestamp > currentTimestamp && p.length > path.length)) {
|
||||||
deviceData.timestamps.delete(p);
|
deviceData.timestamps.delete(p);
|
||||||
deviceData.attributes.delete(p);
|
deviceData.attributes.delete(p);
|
||||||
} else if (t && attributes) {
|
changeFlags |= 1;
|
||||||
|
}
|
||||||
|
else if (attributes && p.length == path.length) {
|
||||||
let currentAttributes = deviceData.attributes.get(p);
|
let currentAttributes = deviceData.attributes.get(p);
|
||||||
if (currentAttributes) {
|
if (currentAttributes) {
|
||||||
let newAttrs;
|
let newAttrs;
|
||||||
for (let attrName in attributes) {
|
for (let attrName in attributes) {
|
||||||
if (attrName in currentAttributes &&
|
if (attrName in currentAttributes &&
|
||||||
attributes[attrName][0] > currentAttributes[attrName][0]) {
|
attributes[attrName] > currentAttributes[attrName][0]) {
|
||||||
if (!newAttrs)
|
changeFlags |= CHANGE_FLAGS[attrName];
|
||||||
newAttrs = JSON.parse(JSON.stringify(currentAttributes));
|
if (!newAttrs) {
|
||||||
|
newAttrs = Object.assign({}, currentAttributes);
|
||||||
|
deviceData.attributes.set(p, newAttrs);
|
||||||
|
}
|
||||||
delete newAttrs[attrName];
|
delete newAttrs[attrName];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newAttrs)
|
|
||||||
deviceData.attributes.set(p, newAttrs);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: For performance, we're merging all changes together rather than
|
||||||
|
// mark changes based the exact parameters affected.
|
||||||
|
for (let k in changeTrackers) {
|
||||||
|
if (changeTrackers[k] & changeFlags)
|
||||||
|
deviceData.changes.add(k);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -226,9 +269,11 @@ function set(deviceData, path, timestamp, attributes, toClear) {
|
|||||||
|
|
||||||
if (path.wildcard)
|
if (path.wildcard)
|
||||||
attributes = undefined;
|
attributes = undefined;
|
||||||
else
|
else if (currentTimestamp)
|
||||||
currentAttributes = deviceData.attributes.get(path);
|
currentAttributes = deviceData.attributes.get(path);
|
||||||
|
|
||||||
|
let changeFlags = 0;
|
||||||
|
|
||||||
if (attributes) {
|
if (attributes) {
|
||||||
if (attributes.value && attributes.value[1]
|
if (attributes.value && attributes.value[1]
|
||||||
&& attributes.value[0] >= (attributes.object ? attributes.object[0] : 0))
|
&& attributes.value[0] >= (attributes.object ? attributes.object[0] : 0))
|
||||||
@@ -238,67 +283,29 @@ function set(deviceData, path, timestamp, attributes, toClear) {
|
|||||||
&& attributes.object[0] >= (attributes.value ? attributes.value[0] : 0))
|
&& attributes.object[0] >= (attributes.value ? attributes.value[0] : 0))
|
||||||
attributes.value = [attributes.object[0]];
|
attributes.value = [attributes.object[0]];
|
||||||
|
|
||||||
if (currentAttributes) {
|
let newAttributes = Object.assign({}, currentAttributes, attributes);
|
||||||
let changeFlags = 0;
|
|
||||||
|
|
||||||
|
if (currentAttributes) {
|
||||||
for (let attrName in attributes) {
|
for (let attrName in attributes) {
|
||||||
timestamp = Math.max(timestamp, attributes[attrName][0]);
|
timestamp = Math.max(timestamp, attributes[attrName][0]);
|
||||||
if (attrName in currentAttributes) {
|
if (!(attrName in currentAttributes))
|
||||||
if (attributes[attrName][0] > currentAttributes[attrName][0]) {
|
changeFlags |= CHANGE_FLAGS[attrName];
|
||||||
changeFlags |= CHANGE_FLAGS[attrName];
|
else if (attributes[attrName][0] <= currentAttributes[attrName][0])
|
||||||
if (compareEquality(attributes[attrName][1], currentAttributes[attrName][1]))
|
newAttributes[attrName] = currentAttributes[attrName];
|
||||||
changeFlags |= CHANGE_FLAGS[attrName] << 1;
|
else if (!compareEquality(attributes[attrName][1], currentAttributes[attrName][1]))
|
||||||
}
|
|
||||||
else
|
|
||||||
delete attributes[attrName];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
changeFlags |= CHANGE_FLAGS[attrName];
|
changeFlags |= CHANGE_FLAGS[attrName];
|
||||||
changeFlags |= CHANGE_FLAGS[attrName] << 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(attributes).length) {
|
|
||||||
if (attributes.object && currentAttributes.object &&
|
|
||||||
attributes.object[1] !== currentAttributes.object[1]) {
|
|
||||||
toClear = toClear || [];
|
|
||||||
toClear.push([path.concat('*'), attributes.object[0]]);
|
|
||||||
}
|
|
||||||
for (let attrName in currentAttributes)
|
|
||||||
if (!(attrName in attributes))
|
|
||||||
attributes[attrName] = currentAttributes[attrName];
|
|
||||||
|
|
||||||
deviceData.attributes.set(path, attributes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timestamp > currentTimestamp) {
|
|
||||||
changeFlags |= 1;
|
|
||||||
deviceData.timestamps.set(path, timestamp);
|
|
||||||
|
|
||||||
if (path.length > 1)
|
|
||||||
toClear = set(deviceData, path.slice(0, path.length - 1), timestamp,
|
|
||||||
{object: [timestamp, 1]}, toClear);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changeFlags)
|
|
||||||
for (let sup of deviceData.paths.find(path, true, false)) {
|
|
||||||
let c = deviceData.trackers.get(sup);
|
|
||||||
for (let k in c)
|
|
||||||
if (c[k] & changeFlags) {
|
|
||||||
deviceData.changes.add(k);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (!(timestamp <= currentTimestamp)) {
|
else
|
||||||
deviceData.timestamps.set(path, timestamp);
|
changeFlags |= 1;
|
||||||
deviceData.attributes.set(path, attributes);
|
|
||||||
|
|
||||||
|
deviceData.attributes.set(path, newAttributes);
|
||||||
|
|
||||||
|
if (!(timestamp <= currentTimestamp)) {
|
||||||
|
deviceData.timestamps.set(path, timestamp);
|
||||||
if (path.length > 1)
|
if (path.length > 1)
|
||||||
toClear = set(deviceData, path.slice(0, path.length - 1), timestamp,
|
toClear = set(deviceData, path.slice(0, path.length - 1), timestamp,
|
||||||
{object: [timestamp, 1]}, toClear);
|
{object: [timestamp, 1]}, toClear);
|
||||||
|
|
||||||
toClear = toClear || [];
|
|
||||||
toClear.push([path, timestamp]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (!(timestamp <= currentTimestamp)) {
|
else if (!(timestamp <= currentTimestamp)) {
|
||||||
@@ -306,43 +313,49 @@ function set(deviceData, path, timestamp, attributes, toClear) {
|
|||||||
|
|
||||||
if (currentAttributes) {
|
if (currentAttributes) {
|
||||||
deviceData.attributes.delete(path);
|
deviceData.attributes.delete(path);
|
||||||
toClear = toClear || [];
|
changeFlags |= 1;
|
||||||
toClear.push([path, timestamp]);
|
|
||||||
}
|
}
|
||||||
|
else if (path.wildcard) {
|
||||||
if (path.wildcard)
|
for (let p of deviceData.paths.find(path, false, true, path.length))
|
||||||
for (let sub of deviceData.paths.find(path, false, true))
|
if (timestamp > deviceData.timestamps.get(p)) {
|
||||||
if (timestamp > deviceData.timestamps.get(sub)) {
|
toClear = toClear || [];
|
||||||
deviceData.timestamps.delete(sub);
|
toClear.push([p, timestamp]);
|
||||||
if (deviceData.attributes.delete(sub)) {
|
|
||||||
toClear = toClear || [];
|
|
||||||
toClear.push([sub, timestamp]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changeFlags) {
|
||||||
|
if (changeFlags & 1) {
|
||||||
|
toClear = toClear || [];
|
||||||
|
toClear.push([path, timestamp, null, changeFlags]);
|
||||||
|
}
|
||||||
|
else if (changeFlags & CHANGE_FLAGS.object) {
|
||||||
|
toClear = toClear || [];
|
||||||
|
toClear.push([path, 0, {object: attributes.object[0]}, changeFlags]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for (let p of deviceData.paths.find(path, true, false, path.length)) {
|
||||||
|
let tracker = deviceData.trackers.get(p);
|
||||||
|
for (let k in tracker) {
|
||||||
|
if (tracker[k] & changeFlags)
|
||||||
|
deviceData.changes.add(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return toClear;
|
return toClear;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function track(deviceData, path, marker, timestamp, attributeTimestamps, exist, attributeValues) {
|
function track(deviceData, path, marker, attributes) {
|
||||||
path = deviceData.paths.add(path);
|
path = deviceData.paths.add(path);
|
||||||
let f = 0;
|
let f = 1;
|
||||||
|
|
||||||
if (timestamp)
|
if (attributes)
|
||||||
f |= 1;
|
for (let attrName of attributes)
|
||||||
|
|
||||||
if (exist)
|
|
||||||
f |= 2;
|
|
||||||
|
|
||||||
if (attributeTimestamps)
|
|
||||||
for (let attrName of attributeTimestamps)
|
|
||||||
f |= CHANGE_FLAGS[attrName];
|
f |= CHANGE_FLAGS[attrName];
|
||||||
|
|
||||||
if (attributeValues)
|
|
||||||
for (let attrName of attributeValues)
|
|
||||||
f |= CHANGE_FLAGS[attrName] << 1;
|
|
||||||
|
|
||||||
let cur = deviceData.trackers.get(path);
|
let cur = deviceData.trackers.get(path);
|
||||||
if (!cur) {
|
if (!cur) {
|
||||||
cur = {};
|
cur = {};
|
||||||
|
|||||||
@@ -26,15 +26,27 @@ function errorToFault(err) {
|
|||||||
if (!err)
|
if (!err)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return {
|
if (!err.name)
|
||||||
|
return {code: "ext", message: `${err}`};
|
||||||
|
|
||||||
|
let fault = {
|
||||||
code: `ext.${err.name}`,
|
code: `ext.${err.name}`,
|
||||||
message: err.message,
|
message: err.message,
|
||||||
detail: {
|
detail: {
|
||||||
name: err.name,
|
name: err.name,
|
||||||
message: err.message,
|
message: err.message
|
||||||
stack: err.stack
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (err.stack) {
|
||||||
|
fault.detail.stack = err.stack;
|
||||||
|
// Trim the stack trace
|
||||||
|
let stackTrimIndex = fault.detail.stack.match(/\s+at\s[^\s]+\s\(.*extension-wrapper\.js:.+\)/);
|
||||||
|
if (stackTrimIndex)
|
||||||
|
fault.detail.stack = fault.detail.stack.slice(0, stackTrimIndex.index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return fault;
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on("uncaughtException", function(err) {
|
process.on("uncaughtException", function(err) {
|
||||||
@@ -55,10 +67,24 @@ process.on("message", function(message) {
|
|||||||
script = require(`${cwd}/${fileName}`);
|
script = require(`${cwd}/${fileName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
script[message[1][0]](message[1].slice(1), function(err, res) {
|
const funcName = message[1][0];
|
||||||
|
|
||||||
|
if (!script[funcName]) {
|
||||||
|
let fault = {
|
||||||
|
code: "ext",
|
||||||
|
message: `No such function '${funcName}' in extension '${fileName}'`
|
||||||
|
};
|
||||||
|
process.send([message[0], fault]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
script[funcName](message[1].slice(1), function(err, res) {
|
||||||
if (!jobs.delete(message[0]))
|
if (!jobs.delete(message[0]))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
process.send([message[0], errorToFault(err), res]);
|
process.send([message[0], errorToFault(err), res]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ignore SIGINT
|
||||||
|
process.on('SIGINT', function() {});
|
||||||
+26
-9
@@ -42,19 +42,23 @@ function run(args, callback) {
|
|||||||
|
|
||||||
if (!processes[scriptName]) {
|
if (!processes[scriptName]) {
|
||||||
let p = childProcess.fork('lib/extension-wrapper', [scriptName]);
|
let p = childProcess.fork('lib/extension-wrapper', [scriptName]);
|
||||||
|
processes[scriptName] = p;
|
||||||
|
|
||||||
p.on('error', function() {
|
p.on('error', function() {
|
||||||
kill(processes[scriptName]);
|
if (processes[scriptName] === p) {
|
||||||
delete processes[scriptName];
|
kill(processes[scriptName]);
|
||||||
|
delete processes[scriptName];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
p.on('disconnect', function() {
|
p.on('disconnect', function() {
|
||||||
kill(processes[scriptName]);
|
if (processes[scriptName] === p) {
|
||||||
delete processes[scriptName];
|
kill(processes[scriptName]);
|
||||||
|
delete processes[scriptName];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
p.on('message', messageHandler);
|
p.on('message', messageHandler);
|
||||||
processes[scriptName] = p;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let id = crypto.randomBytes(8).toString('hex');
|
let id = crypto.randomBytes(8).toString('hex');
|
||||||
@@ -70,7 +74,7 @@ function run(args, callback) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
function kill(process) {
|
function kill(process, callback) {
|
||||||
if (process.signalCode !== null || process.exitCode !== null)
|
if (process.signalCode !== null || process.exitCode !== null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -81,18 +85,31 @@ function kill(process) {
|
|||||||
let t = setInterval(function() {
|
let t = setInterval(function() {
|
||||||
if (process.signalCode !== null || process.exitCode !== null) {
|
if (process.signalCode !== null || process.exitCode !== null) {
|
||||||
clearInterval(t);
|
clearInterval(t);
|
||||||
|
if (callback)
|
||||||
|
callback();
|
||||||
}
|
}
|
||||||
else if (Date.now() > timeToKill) {
|
else if (Date.now() > timeToKill) {
|
||||||
process.kill('SIGKILL');
|
process.kill('SIGKILL');
|
||||||
clearInterval(t);
|
clearInterval(t);
|
||||||
|
if (callback)
|
||||||
|
callback();
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function killAll() {
|
function killAll(callback) {
|
||||||
for (let n in processes)
|
let counter = 1;
|
||||||
kill(processes[n]);
|
for (let n in processes) {
|
||||||
|
++counter;
|
||||||
|
kill(processes[n], function() {
|
||||||
|
if (--counter == 0 && callback)
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
delete processes[n];
|
||||||
|
}
|
||||||
|
if (--counter == 0 && callback)
|
||||||
|
callback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2013-2017 Zaid Abdulla
|
||||||
|
*
|
||||||
|
* This file is part of GenieACS.
|
||||||
|
*
|
||||||
|
* GenieACS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* GenieACS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const WILDCARD_MULTIPLIER = 2;
|
||||||
|
const UNDISCOVERED_DEPTH = 7;
|
||||||
|
|
||||||
|
// Simple heuristic to estimate GPN count given a set of patterns to be
|
||||||
|
// discovered. Used to decide whether to use nextLevel = false in GPN.
|
||||||
|
// gpnPatterns is [pattern, flags]
|
||||||
|
// pattern is a path (array)
|
||||||
|
// flags is an int where its bits mark the segments in the pattern that
|
||||||
|
// need refreshing. Leading 0s indicate that the pattern up to that
|
||||||
|
// point has been discovered.
|
||||||
|
function estimateGpnCount(gpnPatterns, depth = 0) {
|
||||||
|
const children = {};
|
||||||
|
const wildcardChildren = [];
|
||||||
|
let wildcardDiscovered = false;
|
||||||
|
let gpnCount = 0;
|
||||||
|
|
||||||
|
for (let pattern of gpnPatterns) {
|
||||||
|
const path = pattern[0];
|
||||||
|
const flags = pattern[1] >> depth;
|
||||||
|
|
||||||
|
let k = path[depth];
|
||||||
|
|
||||||
|
if (!k) {
|
||||||
|
if (flags & 1)
|
||||||
|
gpnCount = 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags & 1) {
|
||||||
|
gpnCount = 1;
|
||||||
|
|
||||||
|
if (depth > UNDISCOVERED_DEPTH)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else if (k === "*")
|
||||||
|
wildcardDiscovered = true;
|
||||||
|
|
||||||
|
if (k === "*") {
|
||||||
|
wildcardChildren.push(pattern);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
children[k] = children[k] || [];
|
||||||
|
children[k].push(pattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let wildcardGpnCount = 0;
|
||||||
|
if (!wildcardDiscovered && wildcardChildren.length)
|
||||||
|
wildcardGpnCount += estimateGpnCount(wildcardChildren, depth + 1) * WILDCARD_MULTIPLIER;
|
||||||
|
|
||||||
|
for (let k of Object.keys(children)) {
|
||||||
|
let c = estimateGpnCount(children[k].concat(wildcardChildren), depth + 1);
|
||||||
|
wildcardGpnCount -= c;
|
||||||
|
gpnCount += c;
|
||||||
|
}
|
||||||
|
|
||||||
|
gpnCount += Math.max(0, wildcardGpnCount);
|
||||||
|
|
||||||
|
return gpnCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.estimateGpnCount = estimateGpnCount;
|
||||||
@@ -0,0 +1,341 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2013-2017 Zaid Abdulla
|
||||||
|
*
|
||||||
|
* This file is part of GenieACS.
|
||||||
|
*
|
||||||
|
* GenieACS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* GenieACS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const vm = require("vm");
|
||||||
|
const crypto = require("crypto");
|
||||||
|
|
||||||
|
const db = require("./db");
|
||||||
|
const cache = require("./cache");
|
||||||
|
const query = require("./query");
|
||||||
|
const logger = require("./logger");
|
||||||
|
const scheduling = require("./scheduling");
|
||||||
|
|
||||||
|
const REFRESH = 3000;
|
||||||
|
|
||||||
|
let nextRefresh = 1;
|
||||||
|
let hash = null;
|
||||||
|
let presets, provisions, virtualParameters, files;
|
||||||
|
|
||||||
|
|
||||||
|
function computeHash() {
|
||||||
|
// MD5 hash for presets, provisions, virtual parameters for detecting changes
|
||||||
|
let h = crypto.createHash("md5");
|
||||||
|
for (let p of presets) {
|
||||||
|
h.update(JSON.stringify(p.name));
|
||||||
|
h.update(JSON.stringify(p.channel));
|
||||||
|
h.update(JSON.stringify(p.schedule));
|
||||||
|
h.update(JSON.stringify(p.events));
|
||||||
|
h.update(JSON.stringify(p.precondition));
|
||||||
|
h.update(JSON.stringify(p.provisions));
|
||||||
|
}
|
||||||
|
|
||||||
|
let keys;
|
||||||
|
|
||||||
|
keys = Object.keys(provisions).sort();
|
||||||
|
h.update(JSON.stringify(keys));
|
||||||
|
for (let k of keys)
|
||||||
|
h.update(provisions[k].md5);
|
||||||
|
|
||||||
|
keys = Object.keys(virtualParameters).sort();
|
||||||
|
h.update(JSON.stringify(keys));
|
||||||
|
for (let k of keys)
|
||||||
|
h.update(virtualParameters[k].md5);
|
||||||
|
|
||||||
|
hash = h.digest("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function refresh(callback) {
|
||||||
|
if (!nextRefresh)
|
||||||
|
return setTimeout(function() {refresh(callback);}, 20);
|
||||||
|
|
||||||
|
nextRefresh = 0;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
cache.get("presets_hash", function(err, res) {
|
||||||
|
if (err)
|
||||||
|
return callback(err);
|
||||||
|
|
||||||
|
if (hash && res === hash) {
|
||||||
|
nextRefresh = now + (REFRESH - (now % REFRESH));
|
||||||
|
return callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.lock("presets_hash_lock", 3, function(err, unlockOrExtend) {
|
||||||
|
if (err)
|
||||||
|
return callback(err);
|
||||||
|
|
||||||
|
let counter = 3;
|
||||||
|
|
||||||
|
counter += 2;
|
||||||
|
db.getPresets(function(err, res) {
|
||||||
|
if (err) {
|
||||||
|
if (counter & 1)
|
||||||
|
callback(err);
|
||||||
|
return counter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
db.getObjects(function(err, objects) {
|
||||||
|
if (err) {
|
||||||
|
if (counter & 1)
|
||||||
|
callback(err);
|
||||||
|
return counter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no keys are defined, consider all parameters as keys to keep the
|
||||||
|
// same behavior from v1.0
|
||||||
|
for (let obj of objects) {
|
||||||
|
if (!obj["_keys"] || !obj["_keys"].length)
|
||||||
|
obj["_keys"] = Object.keys(obj).filter(k => !k.startsWith("_"));
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sort((a, b) => {
|
||||||
|
if (a.weight === b.weight)
|
||||||
|
return a._id > b._id;
|
||||||
|
else
|
||||||
|
return a.weight - b.weight;
|
||||||
|
});
|
||||||
|
|
||||||
|
presets = [];
|
||||||
|
for (let preset of res) {
|
||||||
|
let schedule = null;
|
||||||
|
if (preset.schedule) {
|
||||||
|
let parts = preset.schedule.trim().split(/\s+/);
|
||||||
|
schedule = {
|
||||||
|
md5: crypto.createHash("md5").update(preset.schedule).digest("hex")
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
schedule.duration = +(parts.shift()) * 1000;
|
||||||
|
schedule.schedule = scheduling.parseCron(parts.join(" "));
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
logger.warn({
|
||||||
|
message: "Invalid preset schedule",
|
||||||
|
preset: preset._id,
|
||||||
|
schedule: preset.schedule
|
||||||
|
});
|
||||||
|
schedule.schedule = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let events = preset.events || {};
|
||||||
|
let precondition = query.convertMongoQueryToFilters(JSON.parse(preset.precondition));
|
||||||
|
let _provisions = preset.provisions || [];
|
||||||
|
|
||||||
|
// Generate provisions from the old configuration format
|
||||||
|
for (let c of preset.configurations) {
|
||||||
|
switch (c.type) {
|
||||||
|
case "age":
|
||||||
|
_provisions.push(["refresh", c.name, c.age]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "value":
|
||||||
|
_provisions.push(["value", c.name, c.value]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "add_tag":
|
||||||
|
_provisions.push(["tag", c.tag, true]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "delete_tag":
|
||||||
|
_provisions.push(["tag", c.tag, false]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "provision":
|
||||||
|
_provisions.push([c.name].concat(c.args || []));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "add_object":
|
||||||
|
for (let obj of objects) {
|
||||||
|
if (obj["_id"] === c.object) {
|
||||||
|
let alias = obj["_keys"].map((k) => `${k}:${JSON.stringify(obj[k])}`).join(",");
|
||||||
|
let p = `${c.name}.[${alias}]`;
|
||||||
|
_provisions.push(["instances", p, 1]);
|
||||||
|
|
||||||
|
for (let k in obj)
|
||||||
|
if (!k.startsWith("_") && !(obj["_keys"].indexOf(k) !== -1))
|
||||||
|
_provisions.push(["value", `${p}.${k}`, obj[k]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "delete_object":
|
||||||
|
for (let obj of objects) {
|
||||||
|
if (obj["_id"] === c.object) {
|
||||||
|
let alias = obj["_keys"].map((k) => `${k}:${JSON.stringify(obj[k])}`).join(",");
|
||||||
|
let p = `${c.name}.[${alias}]`;
|
||||||
|
_provisions.push(["instances", p, 0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (counter & 1)
|
||||||
|
callback(new Error(`Unknown configuration type ${c.type}`));
|
||||||
|
return counter = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
presets.push({
|
||||||
|
name: preset._id,
|
||||||
|
channel: preset.channel || "default",
|
||||||
|
schedule: schedule,
|
||||||
|
events: events,
|
||||||
|
precondition: precondition,
|
||||||
|
provisions: _provisions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((counter -= 2) === 1) {
|
||||||
|
computeHash();
|
||||||
|
cache.set("presets_hash", hash, 300, function(err) {
|
||||||
|
unlockOrExtend(0);
|
||||||
|
nextRefresh = now + (REFRESH - (now % REFRESH));
|
||||||
|
callback(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
counter += 2;
|
||||||
|
db.getProvisions(function(err, res) {
|
||||||
|
if (err) {
|
||||||
|
if (counter & 1)
|
||||||
|
callback(err);
|
||||||
|
return counter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
provisions = {};
|
||||||
|
for (let r of res) {
|
||||||
|
provisions[r._id] = {};
|
||||||
|
provisions[r._id].md5 = crypto.createHash("md5").update(r.script).digest("hex");
|
||||||
|
provisions[r._id].script = new vm.Script(`"use strict";(function(){\n${r.script}\n})();`, {filename: r._id, lineOffset: -1, timeout: 50});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((counter -= 2) === 1) {
|
||||||
|
computeHash();
|
||||||
|
cache.set("presets_hash", hash, 300, function(err) {
|
||||||
|
unlockOrExtend(0);
|
||||||
|
nextRefresh = now + (REFRESH - (now % REFRESH));
|
||||||
|
callback(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
counter += 2;
|
||||||
|
db.getVirtualParameters(function(err, res) {
|
||||||
|
if (err) {
|
||||||
|
if (counter & 1)
|
||||||
|
callback(err);
|
||||||
|
return counter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtualParameters = {};
|
||||||
|
for (let r of res) {
|
||||||
|
virtualParameters[r._id] = {}
|
||||||
|
virtualParameters[r._id].md5 = crypto.createHash("md5").update(r.script).digest("hex");
|
||||||
|
virtualParameters[r._id].script = new vm.Script(`"use strict";(function(){\n${r.script}\n})();`, {filename: r._id, lineOffset: -1, timeout: 50});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((counter -= 2) === 1) {
|
||||||
|
computeHash();
|
||||||
|
cache.set("presets_hash", hash, 300, function(err) {
|
||||||
|
unlockOrExtend(0);
|
||||||
|
nextRefresh = now + (REFRESH - (now % REFRESH));
|
||||||
|
callback(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
counter += 2;
|
||||||
|
db.getFiles(function(err, res) {
|
||||||
|
if (err) {
|
||||||
|
if (counter & 1)
|
||||||
|
callback(err);
|
||||||
|
return counter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
files = {};
|
||||||
|
for (let r of res) {
|
||||||
|
const id = r.filename || r._id.toString();
|
||||||
|
files[id] = {};
|
||||||
|
files[id].length = r.length;
|
||||||
|
files[id].md5 = r.md5;
|
||||||
|
files[id].contentType = r.contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((counter -= 2) === 1) {
|
||||||
|
computeHash();
|
||||||
|
cache.set("presets_hash", hash, 300, function(err) {
|
||||||
|
unlockOrExtend(0);
|
||||||
|
nextRefresh = now + (REFRESH - (now % REFRESH));
|
||||||
|
callback(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if ((counter -= 2) === 1) {
|
||||||
|
computeHash();
|
||||||
|
cache.set("presets_hash", hash, 300, function(err) {
|
||||||
|
unlockOrExtend(0);
|
||||||
|
nextRefresh = now + (REFRESH - (now % REFRESH));
|
||||||
|
callback(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getPresets(callback) {
|
||||||
|
if (Date.now() < nextRefresh)
|
||||||
|
return callback(null, hash, presets);
|
||||||
|
|
||||||
|
refresh(function(err) {
|
||||||
|
callback(err, hash, presets);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getFiles(callback) {
|
||||||
|
if (Date.now() < nextRefresh)
|
||||||
|
return callback(null, hash, files);
|
||||||
|
|
||||||
|
refresh(function(err) {
|
||||||
|
callback(err, hash, files);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getProvisionsAndVirtualParameters(callback) {
|
||||||
|
if (Date.now() < nextRefresh)
|
||||||
|
return callback(null, hash, provisions, virtualParameters);
|
||||||
|
|
||||||
|
refresh(function(err) {
|
||||||
|
callback(err, hash, provisions, virtualParameters);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
exports.getPresets = getPresets;
|
||||||
|
exports.getFiles = getFiles;
|
||||||
|
exports.getProvisionsAndVirtualParameters = getProvisionsAndVirtualParameters;
|
||||||
+4
-3
@@ -165,15 +165,15 @@ function flatten(details) {
|
|||||||
details.informRetryCount = details.rpc.cpeRequest.retryCount;
|
details.informRetryCount = details.rpc.cpeRequest.retryCount;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
details.cpeRequestName = details.rpc.cpeRequest.type;
|
details.cpeRequestName = details.rpc.cpeRequest.name;
|
||||||
if (details.rpc.cpeRequest.commandKey)
|
if (details.rpc.cpeRequest.commandKey)
|
||||||
details.cpeRequestCommandKey = details.rpc.cpeRequest.commandKey;
|
details.cpeRequestCommandKey = details.rpc.cpeRequest.commandKey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (details.rpc.cpeFault) {
|
else if (details.rpc.cpeFault) {
|
||||||
details.acsRequestId = details.rpc.id;
|
details.acsRequestId = details.rpc.id;
|
||||||
details.cpeFaultCode = details.rpc.cpeFault.faultCode;
|
details.cpeFaultCode = details.rpc.cpeFault.detail.faultCode;
|
||||||
details.cpeFaultString = details.rpc.cpeFault.faultString;
|
details.cpeFaultString = details.rpc.cpeFault.detail.faultString;
|
||||||
}
|
}
|
||||||
delete details.rpc;
|
delete details.rpc;
|
||||||
}
|
}
|
||||||
@@ -311,6 +311,7 @@ function accessError(details) {
|
|||||||
|
|
||||||
exports.init = init;
|
exports.init = init;
|
||||||
exports.close = close;
|
exports.close = close;
|
||||||
|
exports.flatten = flatten;
|
||||||
exports.info = info;
|
exports.info = info;
|
||||||
exports.warn = warn;
|
exports.warn = warn;
|
||||||
exports.error = error;
|
exports.error = error;
|
||||||
|
|||||||
+26
-26
@@ -50,6 +50,7 @@ common = require './common'
|
|||||||
db = require './db'
|
db = require './db'
|
||||||
query = require './query'
|
query = require './query'
|
||||||
apiFunctions = require './api-functions'
|
apiFunctions = require './api-functions'
|
||||||
|
cache = require './cache'
|
||||||
|
|
||||||
VERSION = require('../package.json').version
|
VERSION = require('../package.json').version
|
||||||
|
|
||||||
@@ -60,8 +61,8 @@ TAGS_REGEX = /^\/devices\/([a-zA-Z0-9\-\_\%]+)\/tags\/([a-zA-Z0-9\-\_\%]+)\/?$/
|
|||||||
PRESETS_REGEX = /^\/presets\/([a-zA-Z0-9\-\_\%]+)\/?$/
|
PRESETS_REGEX = /^\/presets\/([a-zA-Z0-9\-\_\%]+)\/?$/
|
||||||
OBJECTS_REGEX = /^\/objects\/([a-zA-Z0-9\-\_\%]+)\/?$/
|
OBJECTS_REGEX = /^\/objects\/([a-zA-Z0-9\-\_\%]+)\/?$/
|
||||||
FILES_REGEX = /^\/files\/([a-zA-Z0-9\%\!\*\'\(\)\;\:\@\&\=\+\$\,\?\#\[\]\-\_\.\~]+)\/?$/
|
FILES_REGEX = /^\/files\/([a-zA-Z0-9\%\!\*\'\(\)\;\:\@\&\=\+\$\,\?\#\[\]\-\_\.\~]+)\/?$/
|
||||||
PING_REGEX = /^\/ping\/([a-zA-Z0-9\-\_\.]+)\/?$/
|
PING_REGEX = /^\/ping\/([a-zA-Z0-9\-\_\.\:]+)\/?$/
|
||||||
QUERY_REGEX = /^\/([a-zA-Z0-9_]+s)\/?$/
|
QUERY_REGEX = /^\/([a-zA-Z0-9_]+)\/?$/
|
||||||
DELETE_DEVICE_REGEX = /^\/devices\/([a-zA-Z0-9\-\_\%]+)\/?$/
|
DELETE_DEVICE_REGEX = /^\/devices\/([a-zA-Z0-9\-\_\%]+)\/?$/
|
||||||
PROVISIONS_REGEX = /^\/provisions\/([a-zA-Z0-9\-\_\%]+)\/?$/
|
PROVISIONS_REGEX = /^\/provisions\/([a-zA-Z0-9\-\_\%]+)\/?$/
|
||||||
VIRTUAL_PARAMETERS_REGEX = /^\/virtual_parameters\/([a-zA-Z0-9\-\_\%]+)\/?$/
|
VIRTUAL_PARAMETERS_REGEX = /^\/virtual_parameters\/([a-zA-Z0-9\-\_\%]+)\/?$/
|
||||||
@@ -108,7 +109,7 @@ listener = (request, response) ->
|
|||||||
|
|
||||||
db.presetsCollection.save(preset, (err) ->
|
db.presetsCollection.save(preset, (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
db.redisClient.del('presets_hash', (err) ->
|
cache.del('presets_hash', (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
response.writeHead(200)
|
response.writeHead(200)
|
||||||
response.end()
|
response.end()
|
||||||
@@ -117,7 +118,7 @@ listener = (request, response) ->
|
|||||||
else if request.method == 'DELETE'
|
else if request.method == 'DELETE'
|
||||||
db.presetsCollection.remove({'_id' : presetName}, (err) ->
|
db.presetsCollection.remove({'_id' : presetName}, (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
db.redisClient.del('presets_hash', (err) ->
|
cache.del('presets_hash', (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
response.writeHead(200)
|
response.writeHead(200)
|
||||||
response.end()
|
response.end()
|
||||||
@@ -134,7 +135,7 @@ listener = (request, response) ->
|
|||||||
|
|
||||||
db.objectsCollection.save(object, (err) ->
|
db.objectsCollection.save(object, (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
db.redisClient.del('objects', 'presets_hash', (err) ->
|
cache.del('presets_hash', (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
response.writeHead(200)
|
response.writeHead(200)
|
||||||
response.end()
|
response.end()
|
||||||
@@ -143,7 +144,7 @@ listener = (request, response) ->
|
|||||||
else if request.method == 'DELETE'
|
else if request.method == 'DELETE'
|
||||||
db.objectsCollection.remove({'_id' : objectName}, (err) ->
|
db.objectsCollection.remove({'_id' : objectName}, (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
db.redisClient.del('objects', 'presets_hash', (err) ->
|
cache.del('presets_hash', (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
response.writeHead(200)
|
response.writeHead(200)
|
||||||
response.end()
|
response.end()
|
||||||
@@ -168,7 +169,7 @@ listener = (request, response) ->
|
|||||||
|
|
||||||
db.provisionsCollection.save(object, (err) ->
|
db.provisionsCollection.save(object, (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
db.redisClient.del('presets_hash', (err) ->
|
cache.del('presets_hash', (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
response.writeHead(200)
|
response.writeHead(200)
|
||||||
response.end()
|
response.end()
|
||||||
@@ -177,7 +178,7 @@ listener = (request, response) ->
|
|||||||
else if request.method == 'DELETE'
|
else if request.method == 'DELETE'
|
||||||
db.provisionsCollection.remove({'_id' : provisionName}, (err) ->
|
db.provisionsCollection.remove({'_id' : provisionName}, (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
db.redisClient.del('presets_hash', (err) ->
|
cache.del('presets_hash', (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
response.writeHead(200)
|
response.writeHead(200)
|
||||||
response.end()
|
response.end()
|
||||||
@@ -202,7 +203,7 @@ listener = (request, response) ->
|
|||||||
|
|
||||||
db.virtualParametersCollection.save(object, (err) ->
|
db.virtualParametersCollection.save(object, (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
db.redisClient.del('presets_hash', (err) ->
|
cache.del('presets_hash', (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
response.writeHead(200)
|
response.writeHead(200)
|
||||||
response.end()
|
response.end()
|
||||||
@@ -211,7 +212,7 @@ listener = (request, response) ->
|
|||||||
else if request.method == 'DELETE'
|
else if request.method == 'DELETE'
|
||||||
db.virtualParametersCollection.remove({'_id' : virtualParameterName}, (err) ->
|
db.virtualParametersCollection.remove({'_id' : virtualParameterName}, (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
db.redisClient.del('presets_hash', (err) ->
|
cache.del('presets_hash', (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
response.writeHead(200)
|
response.writeHead(200)
|
||||||
response.end()
|
response.end()
|
||||||
@@ -227,20 +228,14 @@ listener = (request, response) ->
|
|||||||
if request.method == 'POST'
|
if request.method == 'POST'
|
||||||
db.devicesCollection.update({'_id' : deviceId}, {'$addToSet' : {'_tags' : tag}}, {safe: true}, (err) ->
|
db.devicesCollection.update({'_id' : deviceId}, {'$addToSet' : {'_tags' : tag}}, {safe: true}, (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
db.redisClient.del("#{deviceId}_presets_hash", (err) ->
|
response.writeHead(200)
|
||||||
return throwError(err, response) if err
|
response.end()
|
||||||
response.writeHead(200)
|
|
||||||
response.end()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
else if request.method == 'DELETE'
|
else if request.method == 'DELETE'
|
||||||
db.devicesCollection.update({'_id' : deviceId}, {'$pull' : {'_tags' : tag}}, {safe: true}, (err) ->
|
db.devicesCollection.update({'_id' : deviceId}, {'$pull' : {'_tags' : tag}}, {safe: true}, (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
db.redisClient.del("#{deviceId}_presets_hash", (err) ->
|
response.writeHead(200)
|
||||||
return throwError(err, response) if err
|
response.end()
|
||||||
response.writeHead(200)
|
|
||||||
response.end()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
response.writeHead 405, {'Allow': 'POST, DELETE'}
|
response.writeHead 405, {'Allow': 'POST, DELETE'}
|
||||||
@@ -255,14 +250,14 @@ listener = (request, response) ->
|
|||||||
if channel.startsWith('task_')
|
if channel.startsWith('task_')
|
||||||
return db.tasksCollection.remove({_id : new mongodb.ObjectID(channel.slice(5))}, (err) ->
|
return db.tasksCollection.remove({_id : new mongodb.ObjectID(channel.slice(5))}, (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
db.redisClient.del("#{deviceId}_tasks", "#{deviceId}_faults", (err) ->
|
cache.del("#{deviceId}_tasks_faults_operations", (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
response.writeHead(200)
|
response.writeHead(200)
|
||||||
response.end()
|
response.end()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
db.redisClient.del("#{deviceId}_faults", (err) ->
|
cache.del("#{deviceId}_tasks_faults_operations", (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
response.writeHead(200)
|
response.writeHead(200)
|
||||||
response.end()
|
response.end()
|
||||||
@@ -279,7 +274,7 @@ listener = (request, response) ->
|
|||||||
task.device = deviceId
|
task.device = deviceId
|
||||||
apiFunctions.insertTasks(task, (err) ->
|
apiFunctions.insertTasks(task, (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
db.redisClient.del("#{deviceId}_tasks", (err) ->
|
cache.del("#{deviceId}_tasks_faults_operations", (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
|
|
||||||
if urlParts.query.connection_request?
|
if urlParts.query.connection_request?
|
||||||
@@ -335,12 +330,17 @@ listener = (request, response) ->
|
|||||||
if request.method == 'DELETE'
|
if request.method == 'DELETE'
|
||||||
db.tasksCollection.findOne({'_id' : taskId}, {'device' : 1}, (err, task) ->
|
db.tasksCollection.findOne({'_id' : taskId}, {'device' : 1}, (err, task) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
|
if not task?
|
||||||
|
response.writeHead(404)
|
||||||
|
response.end("Task not found")
|
||||||
|
return
|
||||||
|
|
||||||
deviceId = task.device
|
deviceId = task.device
|
||||||
db.tasksCollection.remove({'_id' : taskId}, (err) ->
|
db.tasksCollection.remove({'_id' : taskId}, (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
db.faultsCollection.remove({_id : "#{deviceId}:task_#{String(taskId)}"}, (err) ->
|
db.faultsCollection.remove({_id : "#{deviceId}:task_#{String(taskId)}"}, (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
db.redisClient.del("#{deviceId}_tasks", "#{deviceId}_faults", (err) ->
|
cache.del("#{deviceId}_tasks_faults_operations", (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
response.writeHead(200)
|
response.writeHead(200)
|
||||||
response.end()
|
response.end()
|
||||||
@@ -358,7 +358,7 @@ listener = (request, response) ->
|
|||||||
deviceId = task.device
|
deviceId = task.device
|
||||||
db.faultsCollection.remove({_id : "#{deviceId}:task_#{String(taskId)}"}, (err) ->
|
db.faultsCollection.remove({_id : "#{deviceId}:task_#{String(taskId)}"}, (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
db.redisClient.del("#{deviceId}_faults", (err) ->
|
cache.del("#{deviceId}_tasks_faults_operations", (err) ->
|
||||||
return throwError(err, response) if err
|
return throwError(err, response) if err
|
||||||
response.writeHead(200)
|
response.writeHead(200)
|
||||||
response.end()
|
response.end()
|
||||||
@@ -496,7 +496,7 @@ listener = (request, response) ->
|
|||||||
i = 0
|
i = 0
|
||||||
cur.each((err, item) ->
|
cur.each((err, item) ->
|
||||||
if err
|
if err
|
||||||
throwError(err, response)
|
throwError(err)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
if item?
|
if item?
|
||||||
|
|||||||
+30
-1
@@ -244,10 +244,28 @@ test = (params, query) ->
|
|||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
matchType = (src, dst) ->
|
||||||
|
if typeof src is 'string'
|
||||||
|
return String(dst)
|
||||||
|
else if typeof src is 'number'
|
||||||
|
if +dst == parseFloat(dst)
|
||||||
|
return +dst
|
||||||
|
else if not isNaN(Date.parse(dst))
|
||||||
|
return Date.parse(dst)
|
||||||
|
else if typeof src is 'boolean'
|
||||||
|
v = String(dst).trim()
|
||||||
|
if v is 'true' or v is 'TRUE' or v is 'True' or v is '1'
|
||||||
|
return true
|
||||||
|
else if v is 'false' or v is 'FALSE' or v is 'False' or v is '0'
|
||||||
|
return false
|
||||||
|
|
||||||
|
return dst
|
||||||
|
|
||||||
|
|
||||||
testFilter = (obj, filter) ->
|
testFilter = (obj, filter) ->
|
||||||
for k, v of filter
|
for k, v of filter
|
||||||
[param, op] = k.split(/([^a-zA-Z0-9\-\_\.].*)/, 2)
|
[param, op] = k.split(/([^a-zA-Z0-9\-\_\.].*)/, 2)
|
||||||
val = common.matchType(obj[param], v)
|
val = matchType(obj[param], v)
|
||||||
switch op
|
switch op
|
||||||
when '=', undefined
|
when '=', undefined
|
||||||
return false if obj[param] != val
|
return false if obj[param] != val
|
||||||
@@ -289,6 +307,17 @@ convertMongoQueryToFilters = (query, filters) ->
|
|||||||
v = v.replace(/[^a-zA-Z0-9\-]+/g, '_')
|
v = v.replace(/[^a-zA-Z0-9\-]+/g, '_')
|
||||||
filters["Tags.#{v}"] = true
|
filters["Tags.#{v}"] = true
|
||||||
else
|
else
|
||||||
|
switch k
|
||||||
|
when '_id' then k = 'DeviceID.ID'
|
||||||
|
when '_deviceId._Manufacturer' then k = 'DeviceID.Manufacturer'
|
||||||
|
when '_deviceId._OUI' then k = 'DeviceID.OUI'
|
||||||
|
when '_deviceId._ProductClass' then k = 'DeviceID.ProductClass'
|
||||||
|
when '_deviceId._SerialNumber' then k = 'DeviceID.SerialNumber'
|
||||||
|
when '_lastInform' then k = 'Events.Inform'
|
||||||
|
when '_lastBootstrap' then k = 'Events.0_BOOTSTRAP'
|
||||||
|
when '_lastBoot' then k = 'Events.1_BOOT'
|
||||||
|
when '_registered' then k = 'Events.Registered'
|
||||||
|
|
||||||
if common.typeOf(v) is common.OBJECT_TYPE
|
if common.typeOf(v) is common.OBJECT_TYPE
|
||||||
for kk, vv of v
|
for kk, vv of v
|
||||||
switch kk
|
switch kk
|
||||||
|
|||||||
+51
-15
@@ -26,6 +26,7 @@ const common = require("./common");
|
|||||||
const device = require("./device");
|
const device = require("./device");
|
||||||
const extensions = require("./extensions");
|
const extensions = require("./extensions");
|
||||||
const logger = require("./logger");
|
const logger = require("./logger");
|
||||||
|
const scheduling = require("./scheduling");
|
||||||
|
|
||||||
|
|
||||||
// Used for throwing to exit user script and commit
|
// Used for throwing to exit user script and commit
|
||||||
@@ -40,25 +41,53 @@ let state;
|
|||||||
|
|
||||||
|
|
||||||
class SandboxDate {
|
class SandboxDate {
|
||||||
constructor(arg) {
|
constructor() {
|
||||||
if (arguments.length)
|
if (arguments.length)
|
||||||
return new (Function.prototype.bind.apply(Date, arguments));
|
return new Date(...arguments);
|
||||||
|
|
||||||
return new Date(state.sessionContext.timestamp);
|
return new Date(state.sessionContext.timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
static now() {
|
static now(intervalOrCron, variance) {
|
||||||
return state.sessionContext.timestamp;
|
let t = state.sessionContext.timestamp;
|
||||||
|
|
||||||
|
if (typeof intervalOrCron === "number") {
|
||||||
|
|
||||||
|
if (variance == null)
|
||||||
|
variance = intervalOrCron;
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
if (variance)
|
||||||
|
offset = scheduling.variance(state.sessionContext.deviceId, variance);
|
||||||
|
|
||||||
|
t = scheduling.interval(t, intervalOrCron, offset);
|
||||||
|
}
|
||||||
|
else if (typeof intervalOrCron === "string") {
|
||||||
|
let offset = 0;
|
||||||
|
if (variance)
|
||||||
|
offset = scheduling.variance(state.sessionContext.deviceId, variance);
|
||||||
|
let cron = scheduling.parseCron(intervalOrCron);
|
||||||
|
t = scheduling.cron(t, cron, offset)[0];
|
||||||
|
}
|
||||||
|
else if (intervalOrCron) {
|
||||||
|
throw new Error("Invalid Date.now() argument");
|
||||||
|
}
|
||||||
|
|
||||||
|
return t;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function random() {
|
function random() {
|
||||||
if (!state.rng)
|
if (!state.rng)
|
||||||
state.rng = seedrandom(sessionContext.timestamp);
|
state.rng = seedrandom(state.sessionContext.deviceId);
|
||||||
|
|
||||||
return state.rng();
|
return state.rng();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
random.seed = function(s) {
|
||||||
|
state.rng = seedrandom(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ParameterWrapper {
|
class ParameterWrapper {
|
||||||
constructor(path, attributes, unpacked, unpackedRevision) {
|
constructor(path, attributes, unpacked, unpackedRevision) {
|
||||||
@@ -69,13 +98,13 @@ class ParameterWrapper {
|
|||||||
|
|
||||||
if (state.revision !== unpackedRevision) {
|
if (state.revision !== unpackedRevision) {
|
||||||
unpackedRevision = state.revision;
|
unpackedRevision = state.revision;
|
||||||
unpacked = device.unpack(state.sessionContext.deviceData, path, state.revision + 1);
|
unpacked = device.unpack(state.sessionContext.deviceData, path, state.revision);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!unpacked.length)
|
if (!unpacked.length)
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
||||||
let attr = state.sessionContext.deviceData.attributes.get(unpacked[0], state.revision + 1)[attrName];
|
let attr = state.sessionContext.deviceData.attributes.get(unpacked[0], state.revision)[attrName];
|
||||||
|
|
||||||
if (!attr)
|
if (!attr)
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -90,7 +119,7 @@ class ParameterWrapper {
|
|||||||
|
|
||||||
if (state.revision !== unpackedRevision) {
|
if (state.revision !== unpackedRevision) {
|
||||||
unpackedRevision = state.revision;
|
unpackedRevision = state.revision;
|
||||||
unpacked = device.unpack(state.sessionContext.deviceData, path, state.revision + 1);
|
unpacked = device.unpack(state.sessionContext.deviceData, path, state.revision);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!unpacked.length)
|
if (!unpacked.length)
|
||||||
@@ -105,7 +134,7 @@ class ParameterWrapper {
|
|||||||
|
|
||||||
if (state.revision !== unpackedRevision) {
|
if (state.revision !== unpackedRevision) {
|
||||||
unpackedRevision = state.revision;
|
unpackedRevision = state.revision;
|
||||||
unpacked = device.unpack(state.sessionContext.deviceData, path, state.revision + 1);
|
unpacked = device.unpack(state.sessionContext.deviceData, path, state.revision);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!unpacked.length)
|
if (!unpacked.length)
|
||||||
@@ -120,7 +149,7 @@ class ParameterWrapper {
|
|||||||
|
|
||||||
if (state.revision !== unpackedRevision) {
|
if (state.revision !== unpackedRevision) {
|
||||||
unpackedRevision = state.revision;
|
unpackedRevision = state.revision;
|
||||||
unpacked = device.unpack(state.sessionContext.deviceData, path, state.revision + 1);
|
unpacked = device.unpack(state.sessionContext.deviceData, path, state.revision);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let p of unpacked)
|
for (let p of unpacked)
|
||||||
@@ -243,18 +272,25 @@ function errorToFault(err) {
|
|||||||
if (!err)
|
if (!err)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// Trim the stack trace at the self-executing anonymous wrapper function
|
if (!err.name)
|
||||||
let stackTrimIndex = err.stack.match(/\s+at\s[^\s]+\s+at\s[^\s]+\s\(vm\.js.+\)/).index;
|
return {code: "script", message: `${err}`};
|
||||||
|
|
||||||
return {
|
let fault = {
|
||||||
code: `script.${err.name}`,
|
code: `script.${err.name}`,
|
||||||
message: err.message,
|
message: err.message,
|
||||||
detail: {
|
detail: {
|
||||||
name: err.name,
|
name: err.name,
|
||||||
message: err.message,
|
message: err.message
|
||||||
stack: err.stack.slice(0, stackTrimIndex)
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (err.stack) {
|
||||||
|
// Trim the stack trace at the self-executing anonymous wrapper function
|
||||||
|
let stackTrimIndex = err.stack.match(/\s+at\s[^\s]+\s+at\s[^\s]+\s\(vm\.js.+\)/).index;
|
||||||
|
fault.detail.stack = err.stack.slice(0, stackTrimIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return fault;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2013-2017 Zaid Abdulla
|
||||||
|
*
|
||||||
|
* This file is part of GenieACS.
|
||||||
|
*
|
||||||
|
* GenieACS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* GenieACS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const crypto = require("crypto");
|
||||||
|
|
||||||
|
const later = require("later");
|
||||||
|
|
||||||
|
|
||||||
|
function md532(str) {
|
||||||
|
const digest = crypto
|
||||||
|
.createHash("md5")
|
||||||
|
.update(str)
|
||||||
|
.digest();
|
||||||
|
return (
|
||||||
|
digest.readUInt32LE(0) ^
|
||||||
|
digest.readUInt32LE(4) ^
|
||||||
|
digest.readUInt32LE(8) ^
|
||||||
|
digest.readUInt32LE(12)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function variance(deviceId, variance) {
|
||||||
|
return (md532(deviceId) >>> 0) % variance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function interval(timestamp, interval, offset = 0) {
|
||||||
|
return Math.trunc((timestamp + offset) / interval) * interval - offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function parseCron(cronExp) {
|
||||||
|
let parts = cronExp.trim().split(/\s+/);
|
||||||
|
if (parts.length === 5)
|
||||||
|
parts.unshift("*");
|
||||||
|
|
||||||
|
return later.schedule(later.parse.cron(parts.join(" "), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function cron(timestamp, schedule, offset = 0) {
|
||||||
|
// TODO later.js doesn't throw erorr if expression is invalid!
|
||||||
|
const ret = [0, 0];
|
||||||
|
|
||||||
|
let prev = schedule.prev(1, new Date(timestamp + offset));
|
||||||
|
if (prev)
|
||||||
|
ret[0] = prev.setMilliseconds(0) - offset;
|
||||||
|
|
||||||
|
let next = schedule.next(1, new Date(timestamp + offset + 1000));
|
||||||
|
if (next)
|
||||||
|
ret[1] = next.setMilliseconds(0) - offset;
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
exports.variance = variance;
|
||||||
|
exports.interval = interval;
|
||||||
|
exports.parseCron = parseCron;
|
||||||
|
exports.cron = cron;
|
||||||
+36
-12
@@ -27,6 +27,7 @@ logger = require './logger'
|
|||||||
logger.init(service, require('../package.json').version)
|
logger.init(service, require('../package.json').version)
|
||||||
config = require './config'
|
config = require './config'
|
||||||
db = require './db'
|
db = require './db'
|
||||||
|
cache = require './cache'
|
||||||
extensions = require './extensions'
|
extensions = require './extensions'
|
||||||
|
|
||||||
networkInterface = config.get("#{service.toUpperCase()}_INTERFACE")
|
networkInterface = config.get("#{service.toUpperCase()}_INTERFACE")
|
||||||
@@ -38,30 +39,50 @@ onConnection = require("./#{service}").onConnection
|
|||||||
|
|
||||||
server = null
|
server = null
|
||||||
|
|
||||||
exit = () ->
|
closeServer = (timeout, callback) ->
|
||||||
|
return callback() if not server
|
||||||
|
|
||||||
setTimeout(() ->
|
setTimeout(() ->
|
||||||
process.exit(1)
|
return if not callback
|
||||||
, 30000).unref()
|
|
||||||
|
|
||||||
cluster.worker?.disconnect()
|
# Ignore HTTP requests from connection that may still be open
|
||||||
|
server.removeListener('request', listener)
|
||||||
|
server.setTimeout(1)
|
||||||
|
|
||||||
if not server
|
cb = callback
|
||||||
db.disconnect()
|
callback = null
|
||||||
extensions.killAll()
|
setTimeout(cb, 1000)
|
||||||
logger.close()
|
, timeout).unref()
|
||||||
return
|
|
||||||
|
|
||||||
server.close(() ->
|
server.close(() ->
|
||||||
|
return if not callback
|
||||||
|
|
||||||
|
cb = callback
|
||||||
|
callback = null
|
||||||
|
cb()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
exit = () ->
|
||||||
|
setTimeout(() ->
|
||||||
|
extensions.killAll(() ->
|
||||||
|
process.exit(1)
|
||||||
|
)
|
||||||
|
, 30000).unref()
|
||||||
|
|
||||||
|
closeServer(20000, () ->
|
||||||
db.disconnect()
|
db.disconnect()
|
||||||
|
cache.disconnect()
|
||||||
extensions.killAll()
|
extensions.killAll()
|
||||||
logger.close()
|
cluster.worker?.disconnect()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
process.on('uncaughtException', (err) ->
|
process.on('uncaughtException', (err) ->
|
||||||
logger.error({
|
logger.error({
|
||||||
message: 'Uncaught exception'
|
message: 'Uncaught exception'
|
||||||
exception: err
|
exception: err,
|
||||||
|
pid: process.pid
|
||||||
})
|
})
|
||||||
exit()
|
exit()
|
||||||
)
|
)
|
||||||
@@ -91,7 +112,10 @@ else
|
|||||||
|
|
||||||
db.connect((err) ->
|
db.connect((err) ->
|
||||||
throw err if err
|
throw err if err
|
||||||
server.listen(port, networkInterface)
|
cache.connect((err) ->
|
||||||
|
throw err if err
|
||||||
|
server.listen(port, networkInterface)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+341
-319
File diff suppressed because it is too large.
Load diff
+68
-17
@@ -64,7 +64,6 @@ const NAMESPACES = {
|
|||||||
|
|
||||||
const LIBXMLJS_PARSE_OPTIONS = {
|
const LIBXMLJS_PARSE_OPTIONS = {
|
||||||
"nocdata": true,
|
"nocdata": true,
|
||||||
"noent": true,
|
|
||||||
"recover": config.get("XML_RECOVER"),
|
"recover": config.get("XML_RECOVER"),
|
||||||
"ignore_enc": config.get("XML_IGNORE_ENC")
|
"ignore_enc": config.get("XML_IGNORE_ENC")
|
||||||
};
|
};
|
||||||
@@ -85,6 +84,8 @@ if (XML_IGNORE_NAMESPACE) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let warnings;
|
||||||
|
|
||||||
|
|
||||||
function parseBool(v) {
|
function parseBool(v) {
|
||||||
v = "" + v;
|
v = "" + v;
|
||||||
@@ -93,7 +94,7 @@ function parseBool(v) {
|
|||||||
else if (v === "false" || v === "FALSE" || v === "False" || v === "0")
|
else if (v === "false" || v === "FALSE" || v === "False" || v === "0")
|
||||||
return false;
|
return false;
|
||||||
else
|
else
|
||||||
throw new Error("Invalid boolean value");
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -103,9 +104,21 @@ function event(xml) {
|
|||||||
|
|
||||||
|
|
||||||
function parameterInfoList(xml) {
|
function parameterInfoList(xml) {
|
||||||
return xml.find("ParameterInfoStruct").map(
|
return xml.find("ParameterInfoStruct").map(function(e) {
|
||||||
e => [e.get("Name").text(), parseBool(e.get("Writable").text())]
|
let param = e.get("Name").text();
|
||||||
);
|
let value = e.get("Writable").text();
|
||||||
|
let parsed = parseBool(value);
|
||||||
|
|
||||||
|
if (parsed === null) {
|
||||||
|
warnings.push({
|
||||||
|
message: "Invalid writable attribute",
|
||||||
|
parameter: param
|
||||||
|
});
|
||||||
|
parsed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [param, parsed];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -113,15 +126,41 @@ function parameterValueList(xml) {
|
|||||||
return xml.find("ParameterValueStruct").map(function(e) {
|
return xml.find("ParameterValueStruct").map(function(e) {
|
||||||
const valueElement = e.get("Value");
|
const valueElement = e.get("Value");
|
||||||
const valueType = valueElement.attr("type").value();
|
const valueType = valueElement.attr("type").value();
|
||||||
|
let param = e.get("Name").text();
|
||||||
let value = valueElement.text();
|
let value = valueElement.text();
|
||||||
if (valueType === "xsd:boolean")
|
let parsed = value;
|
||||||
value = parseBool(value);
|
if (valueType === "xsd:boolean") {
|
||||||
else if (valueType === "xsd:int" || valueType === "xsd:unsignedInt")
|
parsed = parseBool(value);
|
||||||
value = parseInt(value);
|
if (parsed === null) {
|
||||||
else if (valueType === "xsd:dateTime")
|
warnings.push({
|
||||||
value = Date.parse(value);
|
message: "Invalid value attribute",
|
||||||
|
parameter: param
|
||||||
|
});
|
||||||
|
parsed = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (valueType === "xsd:int" || valueType === "xsd:unsignedInt") {
|
||||||
|
parsed = parseInt(value);
|
||||||
|
if (isNaN(parsed)) {
|
||||||
|
warnings.push({
|
||||||
|
message: "Invalid value attribute",
|
||||||
|
parameter: param
|
||||||
|
});
|
||||||
|
parsed = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (valueType === "xsd:dateTime") {
|
||||||
|
parsed = Date.parse(value);
|
||||||
|
if (isNaN(parsed)) {
|
||||||
|
warnings.push({
|
||||||
|
message: "Invalid value attribute",
|
||||||
|
parameter: param
|
||||||
|
});
|
||||||
|
parsed = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return [e.get("Name").text(), value, valueType];
|
return [param, parsed, valueType];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +209,17 @@ function SetParameterValues(xml, methodRequest) {
|
|||||||
for (const p of methodRequest.parameterList) {
|
for (const p of methodRequest.parameterList) {
|
||||||
const pvs = paramList.node("ParameterValueStruct");
|
const pvs = paramList.node("ParameterValueStruct");
|
||||||
pvs.node("Name").text(p[0]);
|
pvs.node("Name").text(p[0]);
|
||||||
pvs.node("Value").attr({"xsi:type": p[2]}).text(p[1]);
|
let val = p[1];
|
||||||
|
if (p[2] === 'xsd:dateTime' && typeof val === 'number') {
|
||||||
|
val = (new Date(val)).toISOString();
|
||||||
|
if (methodRequest.DATETIME_MILLISECONDS === false)
|
||||||
|
val = val.replace('.000', '');
|
||||||
|
}
|
||||||
|
if (p[2] === 'xsd:boolean' && typeof val === 'boolean') {
|
||||||
|
if (methodRequest.BOOLEAN_LITERAL === false)
|
||||||
|
val = +val;
|
||||||
|
}
|
||||||
|
pvs.node("Value").attr({"xsi:type": p[2]}).text(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
el.node("ParameterKey").text(methodRequest.parameterKey || "");
|
el.node("ParameterKey").text(methodRequest.parameterKey || "");
|
||||||
@@ -367,13 +416,13 @@ function fault(xml, cwmpVersion) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function request(httpRequest, cwmpVersion) {
|
function request(data, cwmpVersion, warn) {
|
||||||
|
warnings = warn;
|
||||||
|
|
||||||
const rpc = {
|
const rpc = {
|
||||||
cwmpVersion: cwmpVersion
|
cwmpVersion: cwmpVersion
|
||||||
};
|
};
|
||||||
|
|
||||||
const data = httpRequest.getBody();
|
|
||||||
|
|
||||||
if (!data.length)
|
if (!data.length)
|
||||||
return rpc;
|
return rpc;
|
||||||
|
|
||||||
@@ -418,7 +467,9 @@ function request(httpRequest, cwmpVersion) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rpc.id = xml.get("/soap-env:Envelope/soap-env:Header/cwmp:ID", NAMESPACES[rpc.cwmpVersion]).text();
|
let idElement = xml.get("/soap-env:Envelope/soap-env:Header/cwmp:ID", NAMESPACES[rpc.cwmpVersion]);
|
||||||
|
if (idElement)
|
||||||
|
rpc.id = idElement.text();
|
||||||
|
|
||||||
if (methodElement && !(XML_IGNORE_NAMESPACE && methodElement.name() === "Fault")) {
|
if (methodElement && !(XML_IGNORE_NAMESPACE && methodElement.name() === "Fault")) {
|
||||||
switch (methodElement.name()) {
|
switch (methodElement.name()) {
|
||||||
|
|||||||
+13
-1
@@ -36,6 +36,7 @@ class VersionedMap {
|
|||||||
this._sizeDiff = [0];
|
this._sizeDiff = [0];
|
||||||
this._revision = 0;
|
this._revision = 0;
|
||||||
this.map = new Map();
|
this.map = new Map();
|
||||||
|
this.dirty = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -100,6 +101,7 @@ class VersionedMap {
|
|||||||
|
|
||||||
var v = this.map.get(key);
|
var v = this.map.get(key);
|
||||||
if (!v) {
|
if (!v) {
|
||||||
|
this.dirty |= 1 << rev;
|
||||||
for (let i = 0; i < rev; ++ i)
|
for (let i = 0; i < rev; ++ i)
|
||||||
this._sizeDiff[i] -= 1;
|
this._sizeDiff[i] -= 1;
|
||||||
|
|
||||||
@@ -118,12 +120,14 @@ class VersionedMap {
|
|||||||
throw new Error('Cannot modify old revisions');
|
throw new Error('Cannot modify old revisions');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.dirty |= 1 << rev;
|
||||||
if (old === NONEXISTENT)
|
if (old === NONEXISTENT)
|
||||||
++ this._sizeDiff[rev];
|
++ this._sizeDiff[rev];
|
||||||
|
|
||||||
v[rev] = value;
|
v[rev] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
delete(key, rev) {
|
delete(key, rev) {
|
||||||
if (rev == null)
|
if (rev == null)
|
||||||
rev = this._revision;
|
rev = this._revision;
|
||||||
@@ -139,10 +143,11 @@ class VersionedMap {
|
|||||||
if (old === NONEXISTENT)
|
if (old === NONEXISTENT)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
this.dirty |= 1 << rev;
|
||||||
-- this._sizeDiff[rev];
|
-- this._sizeDiff[rev];
|
||||||
v[rev] = NONEXISTENT;
|
v[rev] = NONEXISTENT;
|
||||||
|
|
||||||
return old;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -176,6 +181,7 @@ class VersionedMap {
|
|||||||
for (let i = this._sizeDiff.length; i <= r; ++ i)
|
for (let i = this._sizeDiff.length; i <= r; ++ i)
|
||||||
this._sizeDiff[i] = this._sizeDiff[i - 1];
|
this._sizeDiff[i] = this._sizeDiff[i - 1];
|
||||||
|
|
||||||
|
this.dirty |= 1 << r;
|
||||||
rev[r] = revisions[k];
|
rev[r] = revisions[k];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,6 +189,8 @@ class VersionedMap {
|
|||||||
for (let i = 0; i < minKey; ++ i)
|
for (let i = 0; i < minKey; ++ i)
|
||||||
-- this._sizeDiff[i];
|
-- this._sizeDiff[i];
|
||||||
|
|
||||||
|
this.dirty |= del;
|
||||||
|
|
||||||
for (let i = 0; del > 0; del >>= 1, ++ i)
|
for (let i = 0; del > 0; del >>= 1, ++ i)
|
||||||
if (del & 1)
|
if (del & 1)
|
||||||
rev[i] = NONEXISTENT;
|
rev[i] = NONEXISTENT;
|
||||||
@@ -232,6 +240,10 @@ class VersionedMap {
|
|||||||
this._sizeDiff[revision] = this._sizeDiff[this._sizeDiff.length - 1];
|
this._sizeDiff[revision] = this._sizeDiff[this._sizeDiff.length - 1];
|
||||||
this._sizeDiff.splice(revision + 1, this._sizeDiff.length);
|
this._sizeDiff.splice(revision + 1, this._sizeDiff.length);
|
||||||
|
|
||||||
|
let d = this.dirty >> revision;
|
||||||
|
this.dirty = this.dirty ^ (d << revision);
|
||||||
|
this.dirty |= (+!!d) << revision;
|
||||||
|
|
||||||
for (let pair of this.map) {
|
for (let pair of this.map) {
|
||||||
let k = pair[0];
|
let k = pair[0];
|
||||||
let v = pair[1];
|
let v = pair[1];
|
||||||
|
|||||||
Generated
+926
@@ -0,0 +1,926 @@
|
|||||||
|
{
|
||||||
|
"name": "genieacs",
|
||||||
|
"version": "1.1.2",
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"requires": true,
|
||||||
|
"dependencies": {
|
||||||
|
"abbrev": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
|
||||||
|
},
|
||||||
|
"ajv": {
|
||||||
|
"version": "4.11.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz",
|
||||||
|
"integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=",
|
||||||
|
"requires": {
|
||||||
|
"co": "4.6.0",
|
||||||
|
"json-stable-stringify": "1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ansi-regex": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
|
||||||
|
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
|
||||||
|
},
|
||||||
|
"aproba": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="
|
||||||
|
},
|
||||||
|
"are-we-there-yet": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz",
|
||||||
|
"integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=",
|
||||||
|
"requires": {
|
||||||
|
"delegates": "1.0.0",
|
||||||
|
"readable-stream": "2.3.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"asn1": {
|
||||||
|
"version": "0.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
|
||||||
|
"integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y="
|
||||||
|
},
|
||||||
|
"assert-plus": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz",
|
||||||
|
"integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ="
|
||||||
|
},
|
||||||
|
"asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
|
||||||
|
},
|
||||||
|
"aws-sign2": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz",
|
||||||
|
"integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8="
|
||||||
|
},
|
||||||
|
"aws4": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz",
|
||||||
|
"integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4="
|
||||||
|
},
|
||||||
|
"balanced-match": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||||
|
},
|
||||||
|
"bcrypt-pbkdf": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=",
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"tweetnacl": "0.14.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bindings": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz",
|
||||||
|
"integrity": "sha1-FK1hE4EtLTfXLme0ystLtyZQXxE="
|
||||||
|
},
|
||||||
|
"block-stream": {
|
||||||
|
"version": "0.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
|
||||||
|
"integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=",
|
||||||
|
"requires": {
|
||||||
|
"inherits": "2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"boom": {
|
||||||
|
"version": "2.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz",
|
||||||
|
"integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=",
|
||||||
|
"requires": {
|
||||||
|
"hoek": "2.16.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"brace-expansion": {
|
||||||
|
"version": "1.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
|
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||||
|
"requires": {
|
||||||
|
"balanced-match": "1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bson": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/bson/-/bson-1.0.4.tgz",
|
||||||
|
"integrity": "sha1-k8ENOeqltYQVy8QFLz5T5WKwtyw="
|
||||||
|
},
|
||||||
|
"buffer-shims": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E="
|
||||||
|
},
|
||||||
|
"caseless": {
|
||||||
|
"version": "0.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
|
||||||
|
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
|
||||||
|
},
|
||||||
|
"co": {
|
||||||
|
"version": "4.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||||
|
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ="
|
||||||
|
},
|
||||||
|
"code-point-at": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
||||||
|
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
|
||||||
|
},
|
||||||
|
"combined-stream": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz",
|
||||||
|
"integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=",
|
||||||
|
"requires": {
|
||||||
|
"delayed-stream": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"concat-map": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||||
|
},
|
||||||
|
"console-control-strings": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||||
|
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
|
||||||
|
},
|
||||||
|
"core-util-is": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||||
|
},
|
||||||
|
"cryptiles": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz",
|
||||||
|
"integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=",
|
||||||
|
"requires": {
|
||||||
|
"boom": "2.10.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dashdash": {
|
||||||
|
"version": "1.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||||
|
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
|
||||||
|
"requires": {
|
||||||
|
"assert-plus": "1.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"assert-plus": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"requires": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deep-extend": {
|
||||||
|
"version": "0.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz",
|
||||||
|
"integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8="
|
||||||
|
},
|
||||||
|
"delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
|
||||||
|
},
|
||||||
|
"delegates": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
|
||||||
|
},
|
||||||
|
"detect-libc": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||||
|
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
|
||||||
|
},
|
||||||
|
"ecc-jsbn": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz",
|
||||||
|
"integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=",
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"jsbn": "0.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es6-promise": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz",
|
||||||
|
"integrity": "sha1-7FYjOGgDKQkgcXDDlEjiREndH8Q="
|
||||||
|
},
|
||||||
|
"extend": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
|
||||||
|
"integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ="
|
||||||
|
},
|
||||||
|
"extsprintf": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
|
||||||
|
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
|
||||||
|
},
|
||||||
|
"forever-agent": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
|
||||||
|
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
|
||||||
|
},
|
||||||
|
"form-data": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz",
|
||||||
|
"integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=",
|
||||||
|
"requires": {
|
||||||
|
"asynckit": "0.4.0",
|
||||||
|
"combined-stream": "1.0.6",
|
||||||
|
"mime-types": "2.1.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fs.realpath": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||||
|
},
|
||||||
|
"fstream": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz",
|
||||||
|
"integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=",
|
||||||
|
"requires": {
|
||||||
|
"graceful-fs": "4.1.11",
|
||||||
|
"inherits": "2.0.3",
|
||||||
|
"mkdirp": "0.5.1",
|
||||||
|
"rimraf": "2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fstream-ignore": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz",
|
||||||
|
"integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=",
|
||||||
|
"requires": {
|
||||||
|
"fstream": "1.0.11",
|
||||||
|
"inherits": "2.0.3",
|
||||||
|
"minimatch": "3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gauge": {
|
||||||
|
"version": "2.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
|
||||||
|
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
|
||||||
|
"requires": {
|
||||||
|
"aproba": "1.2.0",
|
||||||
|
"console-control-strings": "1.1.0",
|
||||||
|
"has-unicode": "2.0.1",
|
||||||
|
"object-assign": "4.1.1",
|
||||||
|
"signal-exit": "3.0.2",
|
||||||
|
"string-width": "1.0.2",
|
||||||
|
"strip-ansi": "3.0.1",
|
||||||
|
"wide-align": "1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"getpass": {
|
||||||
|
"version": "0.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
|
||||||
|
"integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
|
||||||
|
"requires": {
|
||||||
|
"assert-plus": "1.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"assert-plus": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"glob": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
|
||||||
|
"requires": {
|
||||||
|
"fs.realpath": "1.0.0",
|
||||||
|
"inflight": "1.0.6",
|
||||||
|
"inherits": "2.0.3",
|
||||||
|
"minimatch": "3.0.4",
|
||||||
|
"once": "1.4.0",
|
||||||
|
"path-is-absolute": "1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graceful-fs": {
|
||||||
|
"version": "4.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
|
||||||
|
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
|
||||||
|
},
|
||||||
|
"har-schema": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz",
|
||||||
|
"integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4="
|
||||||
|
},
|
||||||
|
"har-validator": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz",
|
||||||
|
"integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=",
|
||||||
|
"requires": {
|
||||||
|
"ajv": "4.11.8",
|
||||||
|
"har-schema": "1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"has-unicode": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
||||||
|
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk="
|
||||||
|
},
|
||||||
|
"hawk": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz",
|
||||||
|
"integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=",
|
||||||
|
"requires": {
|
||||||
|
"boom": "2.10.1",
|
||||||
|
"cryptiles": "2.0.5",
|
||||||
|
"hoek": "2.16.3",
|
||||||
|
"sntp": "1.0.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hoek": {
|
||||||
|
"version": "2.16.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz",
|
||||||
|
"integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0="
|
||||||
|
},
|
||||||
|
"http-signature": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz",
|
||||||
|
"integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=",
|
||||||
|
"requires": {
|
||||||
|
"assert-plus": "0.2.0",
|
||||||
|
"jsprim": "1.4.1",
|
||||||
|
"sshpk": "1.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inflight": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
|
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||||
|
"requires": {
|
||||||
|
"once": "1.4.0",
|
||||||
|
"wrappy": "1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inherits": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||||
|
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||||
|
},
|
||||||
|
"ini": {
|
||||||
|
"version": "1.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
|
||||||
|
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
|
||||||
|
},
|
||||||
|
"is-fullwidth-code-point": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
|
||||||
|
"requires": {
|
||||||
|
"number-is-nan": "1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"is-typedarray": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
|
||||||
|
},
|
||||||
|
"isarray": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||||
|
},
|
||||||
|
"isstream": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
||||||
|
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
|
||||||
|
},
|
||||||
|
"jsbn": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
||||||
|
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"json-schema": {
|
||||||
|
"version": "0.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
|
||||||
|
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
|
||||||
|
},
|
||||||
|
"json-stable-stringify": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=",
|
||||||
|
"requires": {
|
||||||
|
"jsonify": "0.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"json-stringify-safe": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||||
|
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
|
||||||
|
},
|
||||||
|
"jsonify": {
|
||||||
|
"version": "0.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
|
||||||
|
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM="
|
||||||
|
},
|
||||||
|
"jsprim": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
|
||||||
|
"integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
|
||||||
|
"requires": {
|
||||||
|
"assert-plus": "1.0.0",
|
||||||
|
"extsprintf": "1.3.0",
|
||||||
|
"json-schema": "0.2.3",
|
||||||
|
"verror": "1.10.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"assert-plus": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"later": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/later/-/later-1.2.0.tgz",
|
||||||
|
"integrity": "sha1-8s9sTdeVbdL1IK3wMpg26YdrrQ8="
|
||||||
|
},
|
||||||
|
"libxmljs": {
|
||||||
|
"version": "0.18.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/libxmljs/-/libxmljs-0.18.7.tgz",
|
||||||
|
"integrity": "sha1-NnPrF8dMuv/e+fM8uD3dgqJQVbc=",
|
||||||
|
"requires": {
|
||||||
|
"bindings": "1.2.1",
|
||||||
|
"nan": "2.5.1",
|
||||||
|
"node-pre-gyp": "0.6.39"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mime-db": {
|
||||||
|
"version": "1.33.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
|
||||||
|
"integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ=="
|
||||||
|
},
|
||||||
|
"mime-types": {
|
||||||
|
"version": "2.1.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
|
||||||
|
"integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
|
||||||
|
"requires": {
|
||||||
|
"mime-db": "1.33.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimatch": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||||
|
"requires": {
|
||||||
|
"brace-expansion": "1.1.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimist": {
|
||||||
|
"version": "0.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
|
||||||
|
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
|
||||||
|
},
|
||||||
|
"mkdirp": {
|
||||||
|
"version": "0.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
||||||
|
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
|
||||||
|
"requires": {
|
||||||
|
"minimist": "0.0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mongodb": {
|
||||||
|
"version": "2.2.34",
|
||||||
|
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.34.tgz",
|
||||||
|
"integrity": "sha1-o09Zu+thdUrsQy3nLD/iFSakTBo=",
|
||||||
|
"requires": {
|
||||||
|
"es6-promise": "3.2.1",
|
||||||
|
"mongodb-core": "2.1.18",
|
||||||
|
"readable-stream": "2.2.7"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"process-nextick-args": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
|
||||||
|
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
|
||||||
|
},
|
||||||
|
"readable-stream": {
|
||||||
|
"version": "2.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz",
|
||||||
|
"integrity": "sha1-BwV6y+JGeyIELTb5jFrVBwVOlbE=",
|
||||||
|
"requires": {
|
||||||
|
"buffer-shims": "1.0.0",
|
||||||
|
"core-util-is": "1.0.2",
|
||||||
|
"inherits": "2.0.3",
|
||||||
|
"isarray": "1.0.0",
|
||||||
|
"process-nextick-args": "1.0.7",
|
||||||
|
"string_decoder": "1.0.3",
|
||||||
|
"util-deprecate": "1.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mongodb-core": {
|
||||||
|
"version": "2.1.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.18.tgz",
|
||||||
|
"integrity": "sha1-TEYTm986HwMt7ZHbSfOO7AFlkFA=",
|
||||||
|
"requires": {
|
||||||
|
"bson": "1.0.4",
|
||||||
|
"require_optional": "1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||||
|
},
|
||||||
|
"nan": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/nan/-/nan-2.5.1.tgz",
|
||||||
|
"integrity": "sha1-1bAWkSUzJql6K77p5hxV2NYDUeI="
|
||||||
|
},
|
||||||
|
"node-pre-gyp": {
|
||||||
|
"version": "0.6.39",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz",
|
||||||
|
"integrity": "sha512-OsJV74qxnvz/AMGgcfZoDaeDXKD3oY3QVIbBmwszTFkRisTSXbMQyn4UWzUMOtA5SVhrBZOTp0wcoSBgfMfMmQ==",
|
||||||
|
"requires": {
|
||||||
|
"detect-libc": "1.0.3",
|
||||||
|
"hawk": "3.1.3",
|
||||||
|
"mkdirp": "0.5.1",
|
||||||
|
"nopt": "4.0.1",
|
||||||
|
"npmlog": "4.1.2",
|
||||||
|
"rc": "1.2.5",
|
||||||
|
"request": "2.81.0",
|
||||||
|
"rimraf": "2.6.2",
|
||||||
|
"semver": "5.5.0",
|
||||||
|
"tar": "2.2.1",
|
||||||
|
"tar-pack": "3.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nopt": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
|
||||||
|
"integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
|
||||||
|
"requires": {
|
||||||
|
"abbrev": "1.1.1",
|
||||||
|
"osenv": "0.1.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"npmlog": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
|
||||||
|
"requires": {
|
||||||
|
"are-we-there-yet": "1.1.4",
|
||||||
|
"console-control-strings": "1.1.0",
|
||||||
|
"gauge": "2.7.4",
|
||||||
|
"set-blocking": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"number-is-nan": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
|
||||||
|
},
|
||||||
|
"oauth-sign": {
|
||||||
|
"version": "0.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz",
|
||||||
|
"integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM="
|
||||||
|
},
|
||||||
|
"object-assign": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
|
||||||
|
},
|
||||||
|
"once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||||
|
"requires": {
|
||||||
|
"wrappy": "1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"os-homedir": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
|
||||||
|
},
|
||||||
|
"os-tmpdir": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
|
||||||
|
},
|
||||||
|
"osenv": {
|
||||||
|
"version": "0.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
|
||||||
|
"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
|
||||||
|
"requires": {
|
||||||
|
"os-homedir": "1.0.2",
|
||||||
|
"os-tmpdir": "1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"path-is-absolute": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
||||||
|
},
|
||||||
|
"performance-now": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz",
|
||||||
|
"integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU="
|
||||||
|
},
|
||||||
|
"process-nextick-args": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw=="
|
||||||
|
},
|
||||||
|
"punycode": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||||
|
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
|
||||||
|
},
|
||||||
|
"qs": {
|
||||||
|
"version": "6.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz",
|
||||||
|
"integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM="
|
||||||
|
},
|
||||||
|
"rc": {
|
||||||
|
"version": "1.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.5.tgz",
|
||||||
|
"integrity": "sha1-J1zWh/bjs2zHVrqibf7oCnkDAf0=",
|
||||||
|
"requires": {
|
||||||
|
"deep-extend": "0.4.2",
|
||||||
|
"ini": "1.3.5",
|
||||||
|
"minimist": "1.2.0",
|
||||||
|
"strip-json-comments": "2.0.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"minimist": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||||
|
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"readable-stream": {
|
||||||
|
"version": "2.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.4.tgz",
|
||||||
|
"integrity": "sha512-vuYxeWYM+fde14+rajzqgeohAI7YoJcHE7kXDAc4Nk0EbuKnJfqtY9YtRkLo/tqkuF7MsBQRhPnPeyjYITp3ZQ==",
|
||||||
|
"requires": {
|
||||||
|
"core-util-is": "1.0.2",
|
||||||
|
"inherits": "2.0.3",
|
||||||
|
"isarray": "1.0.0",
|
||||||
|
"process-nextick-args": "2.0.0",
|
||||||
|
"safe-buffer": "5.1.1",
|
||||||
|
"string_decoder": "1.0.3",
|
||||||
|
"util-deprecate": "1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
"version": "2.81.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz",
|
||||||
|
"integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=",
|
||||||
|
"requires": {
|
||||||
|
"aws-sign2": "0.6.0",
|
||||||
|
"aws4": "1.6.0",
|
||||||
|
"caseless": "0.12.0",
|
||||||
|
"combined-stream": "1.0.6",
|
||||||
|
"extend": "3.0.1",
|
||||||
|
"forever-agent": "0.6.1",
|
||||||
|
"form-data": "2.1.4",
|
||||||
|
"har-validator": "4.2.1",
|
||||||
|
"hawk": "3.1.3",
|
||||||
|
"http-signature": "1.1.1",
|
||||||
|
"is-typedarray": "1.0.0",
|
||||||
|
"isstream": "0.1.2",
|
||||||
|
"json-stringify-safe": "5.0.1",
|
||||||
|
"mime-types": "2.1.18",
|
||||||
|
"oauth-sign": "0.8.2",
|
||||||
|
"performance-now": "0.2.0",
|
||||||
|
"qs": "6.4.0",
|
||||||
|
"safe-buffer": "5.1.1",
|
||||||
|
"stringstream": "0.0.5",
|
||||||
|
"tough-cookie": "2.3.3",
|
||||||
|
"tunnel-agent": "0.6.0",
|
||||||
|
"uuid": "3.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"require_optional": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==",
|
||||||
|
"requires": {
|
||||||
|
"resolve-from": "2.0.0",
|
||||||
|
"semver": "5.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resolve-from": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c="
|
||||||
|
},
|
||||||
|
"rimraf": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
|
||||||
|
"requires": {
|
||||||
|
"glob": "7.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"safe-buffer": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
|
||||||
|
},
|
||||||
|
"seedrandom": {
|
||||||
|
"version": "2.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-2.4.3.tgz",
|
||||||
|
"integrity": "sha1-JDhQTa0zkXMUv/GKxNeU8W1qrsw="
|
||||||
|
},
|
||||||
|
"semver": {
|
||||||
|
"version": "5.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
|
||||||
|
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA=="
|
||||||
|
},
|
||||||
|
"set-blocking": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
|
||||||
|
},
|
||||||
|
"signal-exit": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
|
||||||
|
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
|
||||||
|
},
|
||||||
|
"sntp": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz",
|
||||||
|
"integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=",
|
||||||
|
"requires": {
|
||||||
|
"hoek": "2.16.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sshpk": {
|
||||||
|
"version": "1.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz",
|
||||||
|
"integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=",
|
||||||
|
"requires": {
|
||||||
|
"asn1": "0.2.3",
|
||||||
|
"assert-plus": "1.0.0",
|
||||||
|
"bcrypt-pbkdf": "1.0.1",
|
||||||
|
"dashdash": "1.14.1",
|
||||||
|
"ecc-jsbn": "0.1.1",
|
||||||
|
"getpass": "0.1.7",
|
||||||
|
"jsbn": "0.1.1",
|
||||||
|
"tweetnacl": "0.14.5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"assert-plus": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"string-width": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
|
||||||
|
"requires": {
|
||||||
|
"code-point-at": "1.1.0",
|
||||||
|
"is-fullwidth-code-point": "1.0.0",
|
||||||
|
"strip-ansi": "3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"string_decoder": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
|
||||||
|
"requires": {
|
||||||
|
"safe-buffer": "5.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stringstream": {
|
||||||
|
"version": "0.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
|
||||||
|
"integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg="
|
||||||
|
},
|
||||||
|
"strip-ansi": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||||
|
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
||||||
|
"requires": {
|
||||||
|
"ansi-regex": "2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"strip-json-comments": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||||
|
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
|
||||||
|
},
|
||||||
|
"tar": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz",
|
||||||
|
"integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=",
|
||||||
|
"requires": {
|
||||||
|
"block-stream": "0.0.9",
|
||||||
|
"fstream": "1.0.11",
|
||||||
|
"inherits": "2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tar-pack": {
|
||||||
|
"version": "3.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.1.tgz",
|
||||||
|
"integrity": "sha512-PPRybI9+jM5tjtCbN2cxmmRU7YmqT3Zv/UDy48tAh2XRkLa9bAORtSWLkVc13+GJF+cdTh1yEnHEk3cpTaL5Kg==",
|
||||||
|
"requires": {
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"fstream": "1.0.11",
|
||||||
|
"fstream-ignore": "1.0.5",
|
||||||
|
"once": "1.4.0",
|
||||||
|
"readable-stream": "2.3.4",
|
||||||
|
"rimraf": "2.6.2",
|
||||||
|
"tar": "2.2.1",
|
||||||
|
"uid-number": "0.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tough-cookie": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz",
|
||||||
|
"integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=",
|
||||||
|
"requires": {
|
||||||
|
"punycode": "1.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tunnel-agent": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||||
|
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
|
||||||
|
"requires": {
|
||||||
|
"safe-buffer": "5.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tweetnacl": {
|
||||||
|
"version": "0.14.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||||
|
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"uid-number": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz",
|
||||||
|
"integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE="
|
||||||
|
},
|
||||||
|
"util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||||
|
},
|
||||||
|
"uuid": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA=="
|
||||||
|
},
|
||||||
|
"verror": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
||||||
|
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
|
||||||
|
"requires": {
|
||||||
|
"assert-plus": "1.0.0",
|
||||||
|
"core-util-is": "1.0.2",
|
||||||
|
"extsprintf": "1.3.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"assert-plus": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"wide-align": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==",
|
||||||
|
"requires": {
|
||||||
|
"string-width": "1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-9
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "genieacs",
|
"name": "genieacs",
|
||||||
"version": "1.1.0",
|
"version": "1.1.2",
|
||||||
"description": "A TR-069 Auto Configuration Server (ACS)",
|
"description": "A TR-069 Auto Configuration Server (ACS)",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/zaidka/genieacs.git"
|
"url": "https://github.com/genieacs/genieacs.git"
|
||||||
},
|
},
|
||||||
"homepage": "https://genieacs.com",
|
"homepage": "https://genieacs.com",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -19,18 +19,19 @@
|
|||||||
"bin": "bin"
|
"bin": "bin"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hiredis": "^0.5.0",
|
|
||||||
"later": "^1.2.0",
|
"later": "^1.2.0",
|
||||||
"libxmljs": "^0.18.4",
|
"libxmljs": "^0.18.7",
|
||||||
"mongodb": "^2.2.24",
|
"mongodb": "^2.2.34",
|
||||||
"redis": "^2.6.5",
|
"seedrandom": "^2.4.3"
|
||||||
"seedrandom": "^2.4.2"
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"redis": "^2.6.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"coffeescript": "^1.12.4"
|
"coffeescript": "^1.12.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.2 <8.0"
|
"node": ">=6.9.2 <9.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepublish": "npm run compile",
|
"prepublish": "npm run compile",
|
||||||
|
|||||||
Reference in new issue
Block a user