Compare commits

...
120 Commits
Author SHA1 Message Date
Zaid Abdulla fa1143d2c8 Update README.md 2019-06-04 22:25:00 -07:00
Zaid Abdulla afe27eb3ef 1.1.3 2018-10-23 12:48:18 -07:00
Zaid Abdulla c2b887da89 Update CHANGELOG.md 2018-10-23 11:24:56 -07:00
Zaid Abdulla 085b8f3aaf Update dependencies 2018-10-23 11:20:23 -07:00
mtvs 261e556294 Fix nbi devices task timeout 2018-10-23 11:20:23 -07:00
Paulo George Gomes Bezerra 3f0d1f43eb Using if instead of -n in copy
Alpine linux does not have -n option in copy binary.
To avoid a problem on configure, I replaced the command with an "if".
2018-10-23 11:20:23 -07:00
Dan Morphis 71c91766a6 Fix for crash on null device 2018-10-23 11:20:23 -07:00
Zaid Abdulla 079255d7bd Add support for base64 and hexBinary types 2018-06-04 15:48:52 -07:00
Zaid Abdulla a8b7c180f7 Expire fault when original task expires 2018-04-30 17:12:22 -07:00
Zaid Abdulla 0cae1485b5 Fix query unpacking issue with $not operator 2018-04-30 17:11:05 -07:00
Zaid Abdulla 09defce511 Stricter number parsing when unpacking query 2018-04-30 17:10:01 -07:00
Zaid Abdulla e6caa36aa0 Fix instances being created with undefined values
Happens when creating instances using object templates with nested
parameters.

Resolves: #294
2018-04-16 03:38:46 -07:00
Zaid Abdulla d7997988db Tolerate missing trailing dot in GPN responses
Treat any parameter with children as object even if the device
incorrectly reports it as a regular parameter.
2018-04-16 03:36:58 -07:00
Zaid Abdulla c7cc4c9d5b Check object attribute = 0 when generating GPV 2018-04-16 03:35:39 -07:00
Zaid Abdulla 344f4270f2 Fix infinite loop bug in previous commit 2018-03-28 16:56:49 -07:00
Zaid Abdulla 445b01e895 Prevent redundant extension calls 2018-03-24 00:45:12 -07:00
Zaid Abdulla 52bec5b46f Add config option for max commit iterations 2018-03-24 00:41:17 -07:00
Zaid Abdulla eb806342b8 Fix bug with overlapping timestamp declarations 2018-03-24 00:39:58 -07:00
Zaid Abdulla bc8619e199 Fix error trying to trim a partial stack trace 2018-03-19 23:17:39 -07:00
Zaid Abdulla fb229897f7 1.1.2 2018-02-24 02:41:27 -08:00
Zaid Abdulla 3927b7e383 Update CHANGELOG.md 2018-02-24 02:39:27 -08:00
Zaid Abdulla afae1e3ada Update dependencies and add npm-shrinkwrap.json 2018-02-24 02:30:17 -08:00
Zaid Abdulla f4cf2696f8 Update repo paths in README.md and package.json 2018-02-24 01:30:48 -08:00
Zaid Abdulla 365234d49a Fault when vparam returns invalid value or type 2018-02-22 01:21:14 -08:00
Zaid Abdulla 0d6f9265f7 Trim stack trace from extension errors 2018-02-21 15:28:47 -08:00
Zaid Abdulla 75ddfc38b1 Fault when ext function doesn't exist 2018-02-21 15:22:12 -08:00
Zaid Abdulla 06508d1797 Fix timeout faults after extension crash 2018-02-21 15:07:30 -08:00
Zaid Abdulla 521907fa7d Exit main process after too many exceptions 2018-01-11 06:20:25 -08:00
Zaid Abdulla 2f276c7ce2 Remove retryNow after preset_loop fault 2018-01-11 06:20:25 -08:00
Zaid Abdulla f10056ec2e Can't write exception after response was sent 2018-01-11 06:20:25 -08:00
Zaid Abdulla 779cb1b7a2 Enforce session state
A fix for a security vulnerability reported by Maximilian Hils.
2018-01-11 06:20:25 -08:00
Zaid Abdulla bbe4def5cf No Basic auth for connection requests by default
A fix for a security vulnerability reported by Maximilian Hils.
2018-01-11 06:20:25 -08:00
Zaid Abdulla 88a50a3387 Disable XXE processing in Libxml
A fix for a security vulnerability reported by Maximilian Hils.
2018-01-11 06:20:25 -08:00
Robert Drake 3b2a09a1da Fix IPv6 ping in NBI (#278) 2018-01-05 14:57:23 -08:00
Zaid Abdulla c99496527d Optimize GPN nextLevel heuristic 2017-12-12 16:17:15 -08:00
Zaid Abdulla 32b2d721e5 Avoid extra query caused by unsaved event codes
Since not all event codes are saved (for compatibility reasons), such
event codes (e.g. "2 PERIODIC") causes a new param to be created which
results in a DB query to perform clear() operation.
2017-11-27 16:13:46 -08:00
Zaid Abdulla 945323a7da Avoid extra declare cycle when refreshing vparams 2017-11-27 16:13:15 -08:00
Zaid Abdulla 8b400a45b5 Fix preset_loop fault introduced in #93452d4 2017-11-26 12:40:05 -08:00
Zaid Abdulla 3adee03533 Fix bug where declare returns wrong value 2017-11-21 01:11:44 -08:00
Zaid Abdulla 4288fd5980 Reimplement parsing auth header
Resolves #257
2017-11-13 22:52:14 -08:00
Zaid Abdulla ec57131d51 Log only non-default config 2017-11-12 13:01:39 -08:00
Zaid Abdulla 6bfe590b0d Config to use 1/0 for booleans 2017-11-12 12:56:41 -08:00
Zaid Abdulla f7c1857b6a Config to strip milliseconds from dateTime 2017-11-12 12:29:18 -08:00
Zaid Abdulla 6a9c62b639 Support time discretization in scripts
This can be used to ensure parameter refresh times are aligned (as
opposed to using Date.now() - X). It also allows for possible future
optimizations by exploiting the fact that the value of Date.now(X)
remains static for a given amount of time.

The variance value can be used to shift the discretization threshold
points based on the hash of the device ID for load distribution.

The djb2 hashing algorithm has been replaced with MD5 for better
distribution.
2017-11-10 14:34:24 -08:00
Zaid Abdulla ed873cdd4a Only invalidate syncState after read stage
This is in order to reduce the number of commit iterations especially
during the initial discovery of the data model.
2017-11-04 22:09:52 -07:00
Zaid Abdulla 14bf276112 Clamp param timestamp values to session timestamp 2017-11-04 22:02:22 -07:00
Zaid Abdulla 93452d445f Fix bug updating tags not triggering preset rerun 2017-11-04 20:47:55 -07:00
Zaid Abdulla a436f17888 Replace endless_cycle with more specific faults 2017-10-31 12:58:00 -07:00
Zaid Abdulla 947b4d7118 Fix bug adding duplicate provisions in session 2017-10-29 12:45:26 -07:00
Zaid Abdulla b152071085 Fix exception when logging invalid session
Resolves #269
2017-10-23 17:09:37 -07:00
Zaid Abdulla 9dcd55a84f Log XML parse errors 2017-10-09 00:28:02 -07:00
Zaid Abdulla 342138d92e Log session timeout and connection drop 2017-10-09 00:25:32 -07:00
Zaid Abdulla ef712de9e3 Fix concurrent requests counter leak 2017-09-18 13:51:22 -07:00
Zaid Abdulla b2c5f5ecc9 Handle overload spikes gracefully 2017-09-16 22:43:17 -07:00
Zaid Abdulla f0b9c32ab4 Improve service shutdown process 2017-09-16 22:38:21 -07:00
Zaid Abdulla 3fc13317a2 Include PID in exception logs 2017-09-16 22:36:42 -07:00
Zaid Abdulla 73295729c8 Fix exception when TCP connection drops abruptly 2017-09-16 22:35:46 -07:00
Zaid Abdulla ed3239e4db Fix exception when setting invalid bool value 2017-09-16 22:19:10 -07:00
Zaid Abdulla f2d64d7676 Fix bug deleting specific object instances 2017-09-16 22:16:44 -07:00
Zaid Abdulla 1a83dcc1de Fix issue with declaring values using wildcards
When declaring parameter values using a wildcard path, overlapping
declarations are not applied correctly according to their weight and
execution order. The fix is to unpack first then apply the declarations
to the individual parameters.
2017-09-16 22:14:51 -07:00
Zaid Abdulla 4b17c3c9c3 Fix bug when deleting tags from script 2017-08-22 16:07:04 -07:00
Zaid Abdulla 9cb623c096 Expose random seed function to scripts 2017-08-06 16:56:58 -07:00
Zaid Abdulla 7306f7a8a5 Improve set and clear data model functions 2017-08-04 19:29:25 -07:00
Zaid Abdulla 4eb1c61c4b Fix bug in committing vparam update 2017-07-30 04:57:35 -07:00
Zaid Abdulla 306193e6e9 Don't remove root Tags when empty
It unnecessarily complicates things.
2017-07-30 04:54:55 -07:00
Zaid Abdulla 0ca44d7ffb VersionedMap.delete() should return true/false 2017-07-30 04:52:57 -07:00
Zaid Abdulla 3012f29bb2 Remove redundant delete operator 2017-07-30 04:51:20 -07:00
Zaid Abdulla 8d588507ac Remove unused arg in extensions.killAll() 2017-07-30 04:50:14 -07:00
Zaid Abdulla 7e6eebc486 Avoid deleting Downloads tree after root GPN 2017-07-19 12:38:40 -07:00
Zaid Abdulla 4be709a61b Fix orphan extension processes bug
Issue happens after an exception occurs and existing sessions fail to
finish within the timeout period.
2017-07-17 13:44:49 -07:00
Zaid Abdulla d74c56f182 Show 404 when deleting a non-existing task 2017-07-13 21:56:40 -07:00
Zaid Abdulla 444af51307 Fix vparam bug introduced in 68ea488 2017-07-10 15:00:26 -07:00
Zaid Abdulla bde0bfe616 Fix fault detail when ext throws non-error object 2017-07-04 02:37:08 -07:00
Zaid Abdulla b13b9a54ab Skip tree traversal in case of no declarations 2017-07-04 02:35:58 -07:00
Zaid Abdulla 68ea4887b1 Fix repeating vparam execution
This issue only happens when a device doesn't maintain a single
connection for the entire session.
2017-07-04 02:30:16 -07:00
Zaid Abdulla 75b10bb528 Fix crash in session.deserialize
Resolves #243
2017-07-04 02:26:00 -07:00
Zaid Abdulla f444ff753c Optimize loadPath function 2017-06-30 05:12:31 -07:00
Zaid Abdulla 9dd6980da2 Further fixes related to the previous commit 2017-06-30 03:55:50 -07:00
Zaid Abdulla ec1360217b Some fixes on fetchDevice function
Includes a fix for issue where timestamp for nonexisting root params is
not reported which can result in an unnecessary GPN request.
2017-06-27 01:06:02 -07:00
Zaid Abdulla 8743cebb01 Fix add_object preset ignoring non-key params 2017-06-26 00:51:53 -07:00
Zaid Abdulla ae4526defe Fix expired tasks not getting deleted 2017-06-20 13:54:42 -07:00
Zaid Abdulla 2cba450128 Fix issue causing unnecessary GPN requests 2017-06-16 12:14:00 -07:00
Zaid Abdulla b9fd78390f Fix repeating root GPN requests bug 2017-06-16 12:12:28 -07:00
Zaid Abdulla 9787dd2be7 Fix sync state invalidation bug 2017-06-16 12:10:24 -07:00
Zaid Abdulla defa0c2d41 Fix parameter change tracking bug 2017-06-16 12:07:14 -07:00
Zaid Abdulla 23da93538e Fix declaration generated from download task 2017-06-16 12:06:43 -07:00
Zaid Abdulla 63b7a27021 Show fault if invalid vparam return value 2017-06-10 22:36:14 -07:00
Zaid Abdulla a2f70a8e70 Fix fault code and string in CPE fault log 2017-06-10 22:21:47 -07:00
Zaid Abdulla 480ae5efd6 Fix crash when script throws non-error object 2017-06-05 17:13:52 -07:00
Zaid Abdulla a735f0d9e7 Commit vparam after its declarations are applied 2017-06-05 10:47:00 -07:00
Zaid Abdulla eb812e9754 Remove unnecessary character from QUERY_REGEX 2017-06-01 17:37:10 -07:00
Zaid Abdulla 330c6fc720 Encode download file name with encodeURI 2017-06-01 16:53:09 -07:00
Zaid Abdulla 3ebc249ffa Confirm compatibility with Node 8.x 2017-06-01 16:48:21 -07:00
Zaid Abdulla b38b3cf5ab Default to using Mongo for caching
Stress testing shows that after the recent caching optimization there
seems to be no significant difference in performance when using Mongo
for caching as opposed to Redis. Redis is still supported but is now
optional.

Also removed hiredis dependency since as of node_redis 2.6 the default
parser is faster than hiredis.
2017-06-01 16:34:42 -07:00
Zaid Abdulla b1ed1f4f2f Fix Device.ID param missing on first contact 2017-05-31 23:08:35 -07:00
Zaid Abdulla 8f5effe84c Fix broken path when using nested alias params
Resolves #236
2017-05-30 16:23:13 -07:00
Zaid Abdulla 4d255acfc6 Consider all object params as keys if none exists 2017-05-18 14:15:15 -07:00
Zaid Abdulla 7fdd04343b Fix stack overflow when task name not recognized 2017-05-17 19:14:47 -07:00
Zaid Abdulla 30221f1d4e Don't attempt to cache sessions after exception 2017-05-17 19:13:43 -07:00
Zaid Abdulla 729a4dc6ae Generate download requests before reboot 2017-05-14 18:03:48 -07:00
Zaid Abdulla 87007e20c6 Fix no GPN generated for data model root 2017-05-14 17:43:04 -07:00
Zaid Abdulla 549f4f0323 Accept SOAP requests with no cwmp:ID element
Resolves #233
2017-05-14 14:34:18 -07:00
Zaid Abdulla 3f72bdd84b Fix lock timeout bug 2017-05-12 02:43:21 -07:00
Zaid Abdulla ef8aeda02d Fix TransferComplete fault not saved 2017-05-11 19:01:23 -07:00
Zaid Abdulla 222f8cd701 Fix exception when Download method faults 2017-05-11 19:01:09 -07:00
Zaid Abdulla 624cf01270 Support caching in MongoDB instead of Redis 2017-05-08 10:44:42 -07:00
Zaid Abdulla c6328ce2f4 Avoid unnecessary increment of iteration 2017-05-07 23:50:36 -07:00
Zaid Abdulla 5ab49014b0 Pass current attribute values to vparam scripts 2017-05-01 11:15:30 -07:00
Zaid Abdulla c43933225b Use args variable in vparam scripts 2017-05-01 11:11:28 -07:00
Zaid Abdulla 4ef216679d Fix vparam set declarations being ignored 2017-05-01 11:09:46 -07:00
Zaid Abdulla ca2cce8ab6 Fix exception when adding and removing tags
Only happens if adding and removing tags within the same session.

Resolves: #229
2017-04-21 15:05:08 -07:00
Zaid Abdulla c58f93aeb2 Fix exception when using default provisions 2017-04-19 23:25:57 -07:00
Zaid Abdulla 7e7a2032bb Missing err arg in insertTasks callback 2017-04-12 11:33:09 -07:00
Zaid Abdulla 0d9ca3486b Gracefully handle invalid dateTime param values 2017-04-12 11:03:28 -07:00
Zaid Abdulla dc5c12c8ce Fix bug where values of vparams get mixed up 2017-04-10 15:26:44 -07:00
Zaid Abdulla 09b3d7cb1c Fix missing log details 2017-03-31 11:49:07 -07:00
Spencer Lambert 6338f9f07c Config for UDP connection request port (#221) 2017-03-31 11:14:44 -07:00
Zaid Abdulla 84e79bda9e Fix bug in testing preconditions and events
The bug occurs when the session was serialized and deserialized as a
result of requests coming through a new TCP connection.

Resolves: #219
2017-03-29 17:28:42 -07:00
dvlemplgn 720dbd05d2 Rename FS_IP to FS_HOSTNAME in sample config (#216)
* Rename FS_IP to FS_HOSTNAME in sample config

* Change FS_HOSTNAME to acs.example.com and removed LOG_INFORMS in config-sample.json
2017-03-24 02:14:15 -07:00
Zaid Abdulla 00aacd212b Fix bug in sandbox's Math.random() 2017-03-23 15:59:33 -07:00
30 changed files with 2854 additions and 1228 deletions

No files matched your search

-1
View File
@@ -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
+50
View File
@@ -1,5 +1,55 @@
# Change Log # Change Log
## 1.1.3 (2018-10-23)
- New config option (MAX_COMMIT_ITERATIONS) to avoid max commit iterations
faults for more complex scripts.
- Support base64 and hexBinary parameter types.
- Strict parsing of number values in queries (e.g. "123abc" no longer accepted
as 123).
- Mixing $ne and $not operators is not allowed. Now it throws an error instead
of returning incorrect results.
- When a task expires, any associated fault is also deleted.
- API now accepts 'timeout' argument when posting a task.
- A number of stability fixes.
## 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) ## 1.1.1 (2017-03-23)
- Avoid crashing when connection request credentials are missing. - Avoid crashing when connection request credentials are missing.
+19 -14
View File
@@ -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,19 +62,19 @@ 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 [forum](https://forum.genieacs.com) is a good place to get guidance and
guidance and help from the community. Head on over and join the conversation! help from the community. Head on over and join the conversation! In addition,
In addition, the [wiki](https://github.com/zaidka/genieacs/wiki) provides useful 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
[genieacs.com](https://genieacs.com). [genieacs.com](https://genieacs.com/support/).
## 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 -4
View File
@@ -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
} }
+22 -10
View File
@@ -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,6 +89,10 @@ 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'
if not allowBasicAuth
request.abort()
callback(new Error('Basic HTTP authentication not allowed'))
return
options.headers = {'Authorization' : auth.basic(username or '', password or '')} 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 = {
@@ -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,
@@ -136,6 +152,7 @@ connectionRequest = (deviceId, callback) ->
db.devicesCollection.findOne({_id : deviceId}, proj, (err, device)-> db.devicesCollection.findOne({_id : deviceId}, proj, (err, device)->
return callback(err) if err return callback(err) if err
return callback(new Error("No such device")) if !device
if device.Device? # TR-181 data model if device.Device? # TR-181 data model
connectionRequestUrl = device.Device.ManagementServer.ConnectionRequestURL._value connectionRequestUrl = device.Device.ManagementServer.ConnectionRequestURL._value
@@ -151,7 +168,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 +225,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 +253,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
View File
@@ -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
-310
View File
@@ -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
View File
@@ -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;
+44 -6
View File
@@ -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);
const now = Date.now();
respawnTimestamp = Math.max(now, respawnTimestamp + 2000);
if (respawnTimestamp === now) {
tooManyCrashesTimestamp = now;
cluster.fork(); 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) {
if ((address.addressType === 4 || address.addressType === 6) &&
address.address === SERVICE_ADDRESS && address.port === SERVICE_PORT) {
logger.info({ logger.info({
message: "Worker listening", message: "Worker listening",
pid: worker.process.pid, pid: worker.process.pid,
address: address.address, address: address.address,
port: address.port 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();
}); });
-20
View File
@@ -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]'
@@ -210,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
@@ -244,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
+18 -1
View File
@@ -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,11 @@ 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},
MAX_COMMIT_ITERATIONS : {type : 'int', default: 32},
# XML configuration # XML configuration
XML_RECOVER : {type : 'bool', default : false}, XML_RECOVER : {type : 'bool', default : false},
@@ -185,8 +192,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
+425 -185
View File
@@ -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) ->
@@ -85,6 +120,10 @@ recordFault = (sessionContext, fault, provisions, channels) ->
for channel of channels for channel of channels
provs = sessionContext.faults[channel]?.provisions or [] provs = sessionContext.faults[channel]?.provisions or []
faults[channel] = Object.assign({provisions: provs}, fault) faults[channel] = Object.assign({provisions: provs}, fault)
if channel.startsWith('task_')
taskId = channel.slice(5)
for t in sessionContext.tasks
faults[channel].expiry = t.expiry if t._id == taskId and t.expiry
if sessionContext.retries[channel]? if sessionContext.retries[channel]?
++ sessionContext.retries[channel] ++ sessionContext.retries[channel]
@@ -99,7 +138,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 +193,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 +204,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 +230,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 +254,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 +277,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 +332,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 +369,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,26 +408,29 @@ nextRpc = (sessionContext) ->
session.clearProvisions(sessionContext) session.clearProvisions(sessionContext)
for task in sessionContext.tasks # Clear expired tasks
channel = "task_#{task._id}" sessionContext.tasks = sessionContext.tasks.filter((task) ->
return true if not (task.expiry <= sessionContext.timestamp)
# Delete if expired
if task.expiry <= sessionContext.timestamp
logger.accessInfo({ logger.accessInfo({
sessionContext: sessionContext sessionContext: sessionContext
message: 'Task expired' message: 'Task expired'
task: task task: task
}) })
sessionContext.doneTasks ?= [] sessionContext.doneTasks ?= []
sessionContext.doneTasks.push(String(task._id)) sessionContext.doneTasks.push(task._id)
channel = "task_#{task._id}"
if channel of sessionContext.faults if channel of sessionContext.faults
delete sessionContext.faults[channel] delete sessionContext.faults[channel]
sessionContext.faultsTouched ?= {} sessionContext.faultsTouched ?= {}
sessionContext.faultsTouched[channel] = true sessionContext.faultsTouched[channel] = true
continue
if sessionContext.faults[channel] return false
continue )
task = sessionContext.tasks.find((task) -> !sessionContext.faults["task_#{task._id}"])
return applyPresets(sessionContext) if not task
switch task.name switch task.name
when 'getParameterValues' when 'getParameterValues'
@@ -426,58 +465,94 @@ nextRpc = (sessionContext) ->
session.addProvisions(sessionContext, "task_#{task._id}", session.addProvisions(sessionContext, "task_#{task._id}",
[['instances', task.objectName, 0]]); [['instances', task.objectName, 0]]);
else else
return throwError(new Error('Task name not recognized'), sessionContext.httpResponse) if err return throwError(new Error('Task name not recognized'), sessionContext.httpResponse)
return nextRpc(sessionContext) return nextRpc(sessionContext)
return applyPresets(sessionContext)
) )
sendAcsRequest = (sessionContext, id, acsRequest) -> endSession = (sessionContext, callback) ->
if not acsRequest? saveCache = sessionContext.cacheUntil?
session.end(sessionContext, (err, isNew) ->
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 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 for k of sessionContext.faultsTouched
counter += 2 counter += 2
saveCache = true
if sessionContext.faults[k] if sessionContext.faults[k]
sessionContext.faults[k].retries = sessionContext.retries[k] sessionContext.faults[k].retries = sessionContext.retries[k]
db.saveFault(sessionContext.deviceId, k, sessionContext.faults[k], (err) -> db.saveFault(sessionContext.deviceId, k, sessionContext.faults[k], (err) ->
if err if err
throwError(err, sessionContext.httpResponse) if counter & 1 callback(err) if counter & 1
return counter = 0 return counter = 0
if (counter -= 2) == 1 return callback(null, sessionContext.new) if (counter -= 2) == 1
return writeResponse(sessionContext, soap.response(null))
) )
else else
db.deleteFault(sessionContext.deviceId, k, (err) -> db.deleteFault(sessionContext.deviceId, k, (err) ->
if err if err
throwError(err, sessionContext.httpResponse) if counter & 1 callback(err) if counter & 1
return counter = 0 return counter = 0
if (counter -= 2) == 1 return callback(null, sessionContext.new) if (counter -= 2) == 1
return writeResponse(sessionContext, soap.response(null))
) )
if (counter -= 2) == 1 if saveCache
return writeResponse(sessionContext, soap.response(null)) 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
) )
)
return callback(null, sessionContext.new) if (counter -= 2) == 1
sendAcsRequest = (sessionContext, id, acsRequest) ->
if not acsRequest?
return writeResponse(sessionContext, soap.response(null), true)
if acsRequest.name is 'Download' if acsRequest.name is 'Download'
if not acsRequest.url? if not acsRequest.url?
@@ -487,10 +562,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,145 +593,147 @@ sendAcsRequest = (sessionContext, id, acsRequest) ->
writeResponse(sessionContext, res) writeResponse(sessionContext, res)
getSession = (httpRequest, callback) -> getSession = (connection, sessionId, callback) ->
# Separation by comma is important as some devices don't comform to standard sessionContext = currentSessions.get(connection)
COOKIE_REGEX = /\s*([a-zA-Z0-9\-_]+?)\s*=\s*"?([a-zA-Z0-9\-_]*?)"?\s*(,|;|$)/g
while match = COOKIE_REGEX.exec(httpRequest.headers.cookie)
sessionId = match[2] if match[1] == 'session'
return callback() if not sessionId? if sessionContext
currentSessions.delete(connection)
sessionContext = currentSessions.get(httpRequest.connection)?[sessionId]
if sessionContext?
return callback(null, sessionContext) return callback(null, sessionContext)
return callback() if not sessionId
setTimeout(() -> setTimeout(() ->
db.redisClient.eval('local v=redis.call("get",KEYS[1]);redis.call("del",KEYS[1]);return v;', 1, "session_#{sessionId}", (err, sessionContextString) -> cache.pop("session_#{sessionId}", (err, sessionContextString) ->
return callback(err) if err or not sessionContextString return callback(err) if err or not sessionContextString
session.deserialize(sessionContextString, (err, sessionContext) -> session.deserialize(sessionContextString, callback)
return callback(err) if err
currentSessions.get(httpRequest.connection)[sessionId] = sessionContext
callback(null, sessionContext)
)
)
, 100
) )
, 100)
currentSessions = new WeakMap() currentSessions = new WeakMap()
# When socket closes, store active sessions in redis # When socket closes, store active sessions in cache
onConnection = (socket) -> onConnection = (socket) ->
currentSessions.set(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', () -> socket.on('close', () ->
sessions = currentSessions.get(socket) sessionContext = currentSessions.get(socket)
for sessionId, sessionContext of sessions return if not sessionContext
delete sessionContext.httpRequest currentSessions.delete(socket)
delete sessionContext.httpResponse 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) -> session.serialize(sessionContext, (err, sessionContextString) ->
return throwError(err) if err return throwError(err) if err
# TODO don't set if process is shutting down cache.set("session_#{sessionContext.sessionId}", sessionContextString, Math.ceil(timeout / 1000) + 3, (err) ->
db.redisClient.setex("session_#{sessionId}", sessionContext.timeout, sessionContextString, (err) ->
return throwError(err) if 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
})
listener = (httpRequest, httpResponse) -> stats.totalRequests = 0
if httpRequest.method != 'POST' stats.droppedRequests = 0
httpResponse.writeHead 405, {'Allow': 'POST'} stats.initiatedSessions = 0
httpResponse.end('405 Method Not Allowed') , 10000).unref()
return
if httpRequest.headers['content-encoding']?
switch httpRequest.headers['content-encoding'] getDueTasksAndFaultsAndOperations = (deviceId, timestamp, callback) ->
when 'gzip' cache.get("#{deviceId}_tasks_faults_operations", (err, res) ->
stream = httpRequest.pipe(zlib.createGunzip()) return callback(err) if err
when 'deflate'
stream = httpRequest.pipe(zlib.createInflate()) 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 else
httpResponse.writeHead(415) ttl = config.get('MAX_CACHE_TTL', deviceId)
httpResponse.end('415 Unsupported Media Type')
return
else
stream = httpRequest
chunks = [] cache.set("#{deviceId}_tasks_faults_operations", JSON.stringify(v), ttl, callback)
bytes = 0
stream.on('data', (chunk) ->
chunks.push(chunk)
bytes += chunk.length
)
httpRequest.getBody = () ->
# Write all chunks into a Buffer
body = new Buffer(bytes)
offset = 0
chunks.forEach((chunk) ->
chunk.copy(body, offset, 0, chunk.length)
offset += chunk.length
)
return body
stream.on('end', () ->
rpc = null
parseWarnings = []
getSession(httpRequest, f = (err, sessionContext) ->
return throwError(err, httpResponse) if err
rpc ?= soap.request(httpRequest, sessionContext?.cwmpVersion, parseWarnings)
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
for w in parseWarnings
w.sessionContext = sessionContext
w.rpc = rpc;
logger.accessWarn(w)
if config.get('DEBUG', sessionContext.deviceId)
dump = "# REQUEST #{new Date(Date.now())}\n" + JSON.stringify(httpRequest.headers) + "\n#{httpRequest.getBody()}\n\n"
require('fs').appendFile("./debug/#{sessionContext.deviceId}.dump", dump, (err) ->
return throwError(err) if err
)
processRequest = (sessionContext, rpc) ->
if rpc.cpeRequest? if rpc.cpeRequest?
if rpc.cpeRequest.name is 'Inform' if rpc.cpeRequest.name is 'Inform'
logger.accessInfo({ logger.accessInfo({
@@ -717,6 +794,169 @@ listener = (httpRequest, httpResponse) ->
nextRpc(sessionContext) 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
COOKIE_REGEX = /\s*([a-zA-Z0-9\-_]+?)\s*=\s*"?([a-zA-Z0-9\-_]*?)"?\s*(,|;|$)/g
while match = COOKIE_REGEX.exec(httpRequest.headers.cookie)
sessionId = match[2] if match[1] == 'session'
# If overloaded, ask CPE to retry in 60 seconds
if not sessionId and stats.concurrentRequests > MAX_CONCURRENT_REQUESTS
httpResponse.writeHead(503, {'Retry-after': 60, 'Connection': 'close'})
httpResponse.end('503 Service Unavailable')
stats.droppedRequests += 1
return
if httpRequest.headers['content-encoding']?
switch httpRequest.headers['content-encoding']
when 'gzip'
stream = httpRequest.pipe(zlib.createGunzip())
when 'deflate'
stream = httpRequest.pipe(zlib.createInflate())
else
httpResponse.writeHead(415, {'Connection': 'close'})
httpResponse.end('415 Unsupported Media Type')
return
else
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 = []
bytes = 0
stream.on('data', (chunk) ->
chunks.push(chunk)
bytes += chunk.length
)
stream.on('end', () ->
body = new Buffer(bytes)
offset = 0
chunks.forEach((chunk) ->
chunk.copy(body, offset, 0, chunk.length)
offset += chunk.length
)
parsedRpc = (sessionContext, rpc, parseWarnings) ->
for w in parseWarnings
w.sessionContext = sessionContext
w.rpc = rpc;
logger.accessWarn(w)
if config.get('DEBUG', sessionContext.deviceId)
dump = "# REQUEST #{new Date(Date.now())}\n" + JSON.stringify(httpRequest.headers) + "\n#{body}\n\n"
require('fs').appendFile("./debug/#{sessionContext.deviceId}.dump", dump, (err) ->
return throwError(err) if err
)
processRequest(sessionContext, rpc)
getSession(httpRequest.connection, sessionId, (err, sessionContext) ->
return throwError(err, httpResponse) if err
if sessionContext
sessionContext.httpRequest = httpRequest
sessionContext.httpResponse = httpResponse
if sessionContext.sessionId != sessionId or
sessionContext.lastActivity + sessionContext.timeout * 1000 < Date.now()
logger.accessError({message: 'Invalid session', sessionContext: sessionContext})
httpResponse.writeHead(400, {'Connection': 'close'})
httpResponse.end('Invalid session')
stats.concurrentRequests -= 1
return
else if stats.concurrentRequests > MAX_CONCURRENT_REQUESTS
# Check again just in case device included old session ID from the previous session
httpResponse.writeHead(503, {'Retry-after': 60, 'Connection': 'close'})
httpResponse.end('503 Service Unavailable')
stats.droppedRequests += 1
stats.concurrentRequests -= 1
return
parseWarnings = []
try
rpc = soap.request(body, sessionContext?.cwmpVersion, parseWarnings)
catch err
logger.accessError({
message: 'XML parse error'
parseError: err.message.trim()
sessionContext: sessionContext or {httpRequest: httpRequest, httpResponse: httpResponse}
})
httpResponse.writeHead(400, {'Connection': 'close'})
httpResponse.end(err.message)
stats.concurrentRequests -= 1
return
if sessionContext
if rpc.cpeRequest?.name is 'Inform' or
(!sessionContext.rpcRequest ^ !(rpc.cpeResponse or rpc.cpeFault))
logger.accessError({
message: 'Bad session state'
sessionContext: sessionContext
})
httpResponse.writeHead(400, {'Connection': 'close'})
httpResponse.end('Bad session state')
stats.concurrentRequests -= 1
return
return parsedRpc(sessionContext, rpc, parseWarnings)
if rpc.cpeRequest?.name isnt 'Inform'
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)
)
)
) )
) )
+173 -184
View File
@@ -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
if path.length and projection[path.slice(0, -1).concat("_timestamp").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)]) loaded.push([path, ((1 << path.length) - 1) ^ ((1 << MAX_DEPTH) - 1)])
if projection[path.concat('_writable').join('.')] 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,63 +285,55 @@ 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
res.push([path.concat('*'), obj['_timestamp']]) if obj['_timestamp']
else
for k, v of projTree for k, v of projTree
p = path.concat(k) p = path.concat(k)
res.push([p, obj['_timestamp']]) if obj['_timestamp']
loaded.push([p, ((1 << path.length) - 1) ^ ((1 << MAX_DEPTH) - 1)]) loaded.push([p, ((1 << path.length) - 1) ^ ((1 << MAX_DEPTH) - 1)])
if (obj['_object'] or path.length == 0) and obj['_timestamp']
res.push([p, obj['_timestamp']])
else if (obj['_object'] or path.length == 0) and obj['_timestamp']
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'
if v.length
res.push([['Tags'], timestamp, res.push([['Tags'], timestamp,
{object: [timestamp, 1], writable: [timestamp, 0]}]) {object: [timestamp, 1], writable: [timestamp, 0]}])
for t in v for t in v
@@ -309,8 +342,6 @@ fetchDevice = (id, timestamp, patterns, callback) ->
{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)
redisClient.setex("#{deviceId}_faults", MAX_CACHE_TTL, JSON.stringify(faults), (err) ->
callback(err, faults) 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)
redisClient.setex("#{deviceId}_operations", MAX_CACHE_TTL, JSON.stringify(operations), (err) ->
callback(err, operations) 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
+5 -4
View File
@@ -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,
+113 -98
View File
@@ -22,21 +22,37 @@ 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':
case 'xsd:base64':
case 'xsd:hexBinary':
if (typeof parameterValue[0] !== 'string') { if (typeof parameterValue[0] !== 'string') {
parameterValue[0] = '' + parameterValue[0]; parameterValue[0] = '' + parameterValue[0];
} }
@@ -44,17 +60,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 +83,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,35 +192,58 @@ 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) // Note: For performance, we're merging all changes together rather than
deviceData.attributes.set(p, newAttrs); // mark changes based the exact parameters affected.
} for (let k in changeTrackers) {
} if (changeTrackers[k] & changeFlags)
deviceData.changes.add(k);
} }
} }
@@ -226,9 +271,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,85 +285,63 @@ 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];
if (compareEquality(attributes[attrName][1], currentAttributes[attrName][1])) else if (attributes[attrName][0] <= currentAttributes[attrName][0])
changeFlags |= CHANGE_FLAGS[attrName] << 1; newAttributes[attrName] = currentAttributes[attrName];
else if (!compareEquality(attributes[attrName][1], currentAttributes[attrName][1]))
changeFlags |= CHANGE_FLAGS[attrName];
}
} }
else else
delete attributes[attrName];
}
else {
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; changeFlags |= 1;
deviceData.timestamps.set(path, timestamp);
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);
} }
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)) {
deviceData.timestamps.set(path, timestamp);
deviceData.attributes.set(path, attributes);
if (path.length > 1)
toClear = set(deviceData, path.slice(0, path.length - 1), timestamp,
{object: [timestamp, 1]}, toClear);
toClear = toClear || [];
toClear.push([path, timestamp]);
}
} }
else if (!(timestamp <= currentTimestamp)) { else if (!(timestamp <= currentTimestamp)) {
deviceData.timestamps.set(path, timestamp); deviceData.timestamps.set(path, timestamp);
if (currentAttributes) { if (currentAttributes) {
deviceData.attributes.delete(path); deviceData.attributes.delete(path);
changeFlags |= 1;
}
else if (path.wildcard) {
for (let p of deviceData.paths.find(path, false, true, path.length))
if (timestamp > deviceData.timestamps.get(p)) {
toClear = toClear || []; toClear = toClear || [];
toClear.push([path, timestamp]); toClear.push([p, timestamp]);
}
}
} }
if (path.wildcard) if (changeFlags) {
for (let sub of deviceData.paths.find(path, false, true)) if (changeFlags & 1) {
if (timestamp > deviceData.timestamps.get(sub)) {
deviceData.timestamps.delete(sub);
if (deviceData.attributes.delete(sub)) {
toClear = toClear || []; toClear = toClear || [];
toClear.push([sub, timestamp]); 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);
}
} }
} }
} }
@@ -325,24 +350,14 @@ function set(deviceData, path, timestamp, attributes, 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 = {};
+30 -4
View File
@@ -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() {});
+22 -5
View File
@@ -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() {
if (processes[scriptName] === p) {
kill(processes[scriptName]); kill(processes[scriptName]);
delete processes[scriptName]; delete processes[scriptName];
}
}); });
p.on('disconnect', function() { p.on('disconnect', function() {
if (processes[scriptName] === p) {
kill(processes[scriptName]); kill(processes[scriptName]);
delete 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();
} }
+82
View File
@@ -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;
+356
View File
@@ -0,0 +1,356 @@
/**
* 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 flattenObject(src, prefix = "", dst = {}) {
for (let k of Object.keys(src)) {
let v = src[k];
if (typeof v === "object" && !Array.isArray(v))
flattenObject(v, `${prefix}${k}.`, dst);
else
dst[`${prefix}${k}`] = v;
}
return dst;
}
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;
}
objects = objects.map(obj => {
// Flatten object
obj = flattenObject(obj);
// If no keys are defined, consider all parameters as keys to keep the
// same behavior from v1.0
if (!obj["_keys"] || !obj["_keys"].length)
obj["_keys"] = Object.keys(obj).filter(k => !k.startsWith("_"));
return obj
})
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
View File
@@ -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;
+24 -23
View File
@@ -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()
@@ -226,22 +227,16 @@ listener = (request, response) ->
tag = querystring.unescape(r[2]) tag = querystring.unescape(r[2])
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
db.redisClient.del("#{deviceId}_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()
) )
)
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
db.redisClient.del("#{deviceId}_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()
) )
)
else else
response.writeHead 405, {'Allow': 'POST, DELETE'} response.writeHead 405, {'Allow': 'POST, DELETE'}
response.end('405 Method Not Allowed') response.end('405 Method Not Allowed')
@@ -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?
@@ -288,7 +283,8 @@ listener = (request, response) ->
response.writeHead(202, err.message, {'Content-Type' : 'application/json'}) response.writeHead(202, err.message, {'Content-Type' : 'application/json'})
response.end(JSON.stringify(task)) response.end(JSON.stringify(task))
else else
apiFunctions.watchTask(deviceId, task._id, config.get('DEVICE_ONLINE_THRESHOLD', deviceId), (err, status) -> taskTimeout = urlParts.query.timeout? and parseInt(urlParts.query.timeout) or config.get('DEVICE_ONLINE_THRESHOLD', deviceId)
apiFunctions.watchTask(deviceId, task._id, taskTimeout, (err, status) ->
return throwError(err, response) if err return throwError(err, response) if err
if status is 'timeout' if status is 'timeout'
@@ -335,12 +331,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 +359,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 +497,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?
+5 -4
View File
@@ -70,9 +70,8 @@ normalize = (input) ->
if (m = /^\/(.*?)\/(g?i?m?y?)$/.exec(input)) if (m = /^\/(.*?)\/(g?i?m?y?)$/.exec(input))
vals.push({'$regex' : new RegExp(m[1], m[2])}) vals.push({'$regex' : new RegExp(m[1], m[2])})
f = parseFloat(input) if +input == parseFloat(input)
if not isNaN(f) vals.push(+input)
vals.push(f)
d = new Date(input) d = new Date(input)
if input.length >= 8 and d.getFullYear() > 1983 if input.length >= 8 and d.getFullYear() > 1983
@@ -150,7 +149,9 @@ expand = (query) ->
conditions = permute(k, v) conditions = permute(k, v)
if conditions.length > 1 if conditions.length > 1
new_query['$and'] ?= [] new_query['$and'] ?= []
if v?['$ne']? if v?['$ne']? or v?['$not']?
if Object.keys(v) > 1
throw new Error("Cannot mix $ne or $not with other operators")
for c in conditions for c in conditions
new_query['$and'].push(c) new_query['$and'].push(c)
else else
+77 -17
View File
@@ -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
@@ -38,27 +39,78 @@ const context = vm.createContext();
let state; let state;
const runningExtensions = new WeakMap();
function runExtension(sessionContext, key, extCall, callback) {
let re = runningExtensions.get(sessionContext);
if (!re) {
re = {};
runningExtensions.set(sessionContext, re);
}
if (re[key]) {
re[key].push(callback);
}
else {
re[key] = [callback];
extensions.run(extCall, function(err, fault, res) {
const callbacks = re[key];
delete re[key];
if (!err && !fault)
sessionContext.extensionsCache[key] = res;
for (let c of callbacks)
c(err, fault);
});
}
}
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 +121,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 +142,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 +157,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 +172,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 +295,27 @@ 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) {
fault.detail.stack = err.stack;
// Trim the stack trace at the self-executing anonymous wrapper function
let stackTrimIndex = fault.detail.stack.match(/\s+at\s[^\s]+\s+at\s[^\s]+\s\(vm\.js.+\)/);
if (stackTrimIndex)
fault.detail.stack = fault.detail.stack.slice(0, stackTrimIndex.index);
} }
return fault;
} }
@@ -302,13 +363,12 @@ function run(script, globals, sessionContext, startRevision, maxRevision, callba
let counter = 3; let counter = 3;
for (let key of Object.keys(_state.extensions)) { for (let key of Object.keys(_state.extensions)) {
counter += 2; counter += 2;
extensions.run(_state.extensions[key], function(err, fault, res) { runExtension(_state.sessionContext, key, _state.extensions[key], function(err, fault) {
if (err || fault) { if (err || fault) {
if (counter & 1) if (counter & 1)
callback(err, fault); callback(err, fault);
return counter = 0; return counter = 0;
} }
_state.sessionContext.extensionsCache[key] = res;
if ((counter -= 2) === 1) if ((counter -= 2) === 1)
if (status === 2) if (status === 2)
return run.apply(null, args); return run.apply(null, args);
+78
View File
@@ -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;
+35 -11
View File
@@ -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()
) )
@@ -90,9 +111,12 @@ else
server.on('connection', onConnection) if onConnection? server.on('connection', onConnection) if onConnection?
db.connect((err) -> db.connect((err) ->
throw err if err
cache.connect((err) ->
throw err if err throw err if err
server.listen(port, networkInterface) server.listen(port, networkInterface)
) )
)
process.on('SIGINT', exit) process.on('SIGINT', exit)
+326 -303
View File
File diff suppressed because it is too large. Load diff
+15 -6
View File
@@ -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")
}; };
@@ -210,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 || "");
@@ -407,15 +416,13 @@ function fault(xml, cwmpVersion) {
} }
function request(httpRequest, cwmpVersion, warn) { function request(data, cwmpVersion, warn) {
warnings = 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;
@@ -460,7 +467,9 @@ function request(httpRequest, cwmpVersion, warn) {
} }
} }
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
View File
@@ -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];
+625
View File
@@ -0,0 +1,625 @@
{
"name": "genieacs",
"version": "1.1.3",
"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=="
},
"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"
}
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"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="
},
"chownr": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz",
"integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g=="
},
"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="
},
"coffeescript": {
"version": "1.12.7",
"resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-1.12.7.tgz",
"integrity": "sha512-pLXHFxQMPklVoEekowk8b3erNynC+DVJzChxS/LCBBgR6/8AJkHivkm//zbowcfc7BTCAjryuhx6gPqPRfsFoA==",
"dev": true
},
"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="
},
"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="
},
"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="
},
"double-ended-queue": {
"version": "2.1.0-0",
"resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz",
"integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=",
"optional": true
},
"es6-promise": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz",
"integrity": "sha1-7FYjOGgDKQkgcXDDlEjiREndH8Q="
},
"fs-minipass": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz",
"integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==",
"requires": {
"minipass": "2.3.4"
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"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"
}
},
"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"
}
},
"has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk="
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": "2.1.2"
}
},
"ignore-walk": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz",
"integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==",
"requires": {
"minimatch": "3.0.4"
}
},
"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"
}
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"later": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/later/-/later-1.2.0.tgz",
"integrity": "sha1-8s9sTdeVbdL1IK3wMpg26YdrrQ8="
},
"libxmljs": {
"version": "0.18.8",
"resolved": "https://registry.npmjs.org/libxmljs/-/libxmljs-0.18.8.tgz",
"integrity": "sha1-sKB1EqASkLYkBgDWwrwzo8cJdtY=",
"requires": {
"bindings": "1.3.0",
"nan": "2.10.0",
"node-pre-gyp": "0.9.1"
},
"dependencies": {
"bindings": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.0.tgz",
"integrity": "sha512-DpLh5EzMR2kzvX1KIlVC0VkC3iZtHKTgdtZ0a3pglBZdaQFjt5S9g9xd1lE+YvXyfd6mtCeRnrUfOLYiTMlNSw=="
},
"nan": {
"version": "2.10.0",
"resolved": "http://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA=="
},
"node-pre-gyp": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.9.1.tgz",
"integrity": "sha1-8RwHUW3ZL4cZnbx+GDjqt81WyeA=",
"requires": {
"detect-libc": "1.0.3",
"mkdirp": "0.5.1",
"needle": "2.2.4",
"nopt": "4.0.1",
"npm-packlist": "1.1.12",
"npmlog": "4.1.2",
"rc": "1.2.5",
"rimraf": "2.6.2",
"semver": "5.5.0",
"tar": "4.4.6"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"tar": {
"version": "4.4.6",
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.6.tgz",
"integrity": "sha512-tMkTnh9EdzxyfW+6GK6fCahagXsnYk6kE6S9Gr9pjVdys769+laCTbodXDhPAjzVtEBazRgP0gYqOjnk9dQzLg==",
"requires": {
"chownr": "1.1.1",
"fs-minipass": "1.2.5",
"minipass": "2.3.4",
"minizlib": "1.1.1",
"mkdirp": "0.5.1",
"safe-buffer": "5.1.2",
"yallist": "3.0.2"
}
}
}
},
"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="
},
"minipass": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.4.tgz",
"integrity": "sha512-mlouk1OHlaUE8Odt1drMtG1bAJA4ZA6B/ehysgV0LUIrDHdKgo1KorZq3pK0b/7Z7LJIQ12MNM6aC+Tn6lUZ5w==",
"requires": {
"safe-buffer": "5.1.2",
"yallist": "3.0.2"
},
"dependencies": {
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
}
}
},
"minizlib": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.1.tgz",
"integrity": "sha512-TrfjCjk4jLhcJyGMYymBH6oTXcWjYbUAXTHDbtnWHjZC25h0cdajHuPE1zxb4DVmu8crfh+HwH/WMuyLG0nHBg==",
"requires": {
"minipass": "2.3.4"
}
},
"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.36",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.36.tgz",
"integrity": "sha512-P2SBLQ8Z0PVx71ngoXwo12+FiSfbNfGOClAao03/bant5DgLNkOPAck5IaJcEk4gKlQhDEURzfR3xuBG1/B+IA==",
"requires": {
"es6-promise": "3.2.1",
"mongodb-core": "2.1.20",
"readable-stream": "2.2.7"
},
"dependencies": {
"mongodb-core": {
"version": "2.1.20",
"resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.20.tgz",
"integrity": "sha512-IN57CX5/Q1bhDq6ShAR6gIv4koFsZP7L8WOK1S0lR0pVDQaScffSMV5jxubLsmZ7J+UdqmykKw4r9hG3XQEGgQ==",
"requires": {
"bson": "1.0.4",
"require_optional": "1.0.1"
}
},
"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"
}
}
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"needle": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.2.4.tgz",
"integrity": "sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA==",
"requires": {
"debug": "2.6.9",
"iconv-lite": "0.4.24",
"sax": "1.2.4"
}
},
"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"
}
},
"npm-bundled": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz",
"integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g=="
},
"npm-packlist": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.12.tgz",
"integrity": "sha512-WJKFOVMeAlsU/pjXuqVdzU0WfgtIBCupkEVwn+1Y0ERAbUfWw8R4GjgVbaKnUjRoD2FoQbHOCbOyT5Mbs9Lw4g==",
"requires": {
"ignore-walk": "3.0.1",
"npm-bundled": "1.0.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="
},
"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="
},
"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=="
},
"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"
}
},
"redis": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz",
"integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==",
"optional": true,
"requires": {
"double-ended-queue": "2.1.0-0",
"redis-commands": "1.4.0",
"redis-parser": "2.6.0"
}
},
"redis-commands": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.4.0.tgz",
"integrity": "sha512-cu8EF+MtkwI4DLIT0x9P8qNTLFhQD4jLfxLR0cCNkeGzs87FN6879JOJwNQR/1zD7aSYNbU0hgsV9zGY71Itvw==",
"optional": true
},
"redis-parser": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz",
"integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=",
"optional": true
},
"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=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"seedrandom": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-2.4.4.tgz",
"integrity": "sha512-9A+PDmgm+2du77B5i0Ip2cxOqqHjgNxnBgglxLcX78A2D6c2rTo61z4jnVABpF4cKeDMDG+cmXXvdnqse2VqMA=="
},
"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="
},
"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"
}
},
"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="
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"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="
},
"yallist": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz",
"integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k="
}
}
}
+11 -10
View File
@@ -1,10 +1,10 @@
{ {
"name": "genieacs", "name": "genieacs",
"version": "1.1.1", "version": "1.1.3",
"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,23 +19,24 @@
"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.8",
"mongodb": "^2.2.24", "mongodb": "^2.2.36",
"redis": "^2.6.5", "seedrandom": "^2.4.4"
"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",
"compile": "coffee -cb lib/*.coffee", "compile": "coffee -cb lib/*.coffee",
"configure": "cp -n ./config/config-sample.json ./config/config.json", "configure": "if [ ! -f ./config/config.json ]; then cp ./config/config-sample.json ./config/config.json; fi",
"install": "npm run configure", "install": "npm run configure",
"test": "node test" "test": "node test"
} }