Compare commits

...
66 Commits
Author SHA1 Message Date
Zaid Abdulla a47811d20f 1.0.5 2015-11-22 01:46:35 +03:00
Zaid Abdulla 86833a7011 Use a valid SPDX license expression 2015-11-07 20:35:10 +03:00
Zaid Abdulla 7032f273db Merge pull request #110 from shweelan/v1.0
Respond with 404 when manipulating resources that don't exist.
2015-09-26 21:57:19 +03:00
Shweelan L. Samson 6ae4728ded response 404 when device not found or task not found or object not found, when update or delete 2015-09-07 14:11:20 +03:00
Zaid Abdulla cda98e3145 Return task object including fault details in case of fault. Return reason for connection request failure. 2015-07-09 22:22:04 +03:00
Zaid Abdulla b2609c228a Fixed error details not being properly saved in fault property of customCommand tasks. 2015-07-05 03:44:44 +03:00
Zaid Abdulla 52fb326942 Ensure customCommand init tasks triggered by inform run after any refreshObject task. 2015-07-05 01:39:54 +03:00
Zaid Abdulla 28c5dc0d46 Fixed bug where ordering of key parameters in object definition may affect preset behavior. 2015-07-03 01:42:49 +03:00
Zaid Abdulla a02e3ecf17 Fixed the incorrect way used to return error resonses in NBI. 2015-07-01 05:11:30 +03:00
Zaid Abdulla b91bcb7442 1.0.4 2015-05-31 19:40:33 +03:00
Zaid Abdulla a0c0a7938b Updated dependency version ranges. 2015-05-31 19:39:12 +03:00
Dan Morphis 82f354bf2b Set the status code to 400 when invalid query params are sent 2015-05-31 13:38:47 +03:00
Dan Morphis 7f64b567db Don't crash if the query params are invalid json 2015-05-31 13:38:47 +03:00
Dan Morphis b17f842de7 Return the task object when adding a task 2015-05-31 13:38:15 +03:00
Zaid Abdulla ff68553bd7 No longer storing or relying on object instance names as that proved to be unnecessary and can potentially cause unpredictable behavior in certain edge cases. 2015-05-31 03:04:26 +03:00
Zaid Abdulla 22b11d6103 Updated README file. 2015-05-31 00:24:05 +03:00
Zaid Abdulla 00987ea8e2 Searching by MAC now works regardless of whether the device's MAC is lower or upper case. 2015-05-30 01:49:52 +03:00
Zaid Abdulla f0a1289818 Modified sample configuration for wider device compatibility. 2015-05-29 14:21:16 +03:00
Zaid Abdulla 38840d5b8d Added debug dump files to .gitignore. 2015-05-29 14:12:19 +03:00
Zaid Abdulla 74008bc7de Implemented a workaround for certain devices dropping connection after digest authentication challege in connection requests. 2015-05-29 14:11:05 +03:00
Zaid Abdulla 305558f186 Fixed ability to query tasks collection using task ID. Fixes #68. 2015-05-26 12:54:04 +03:00
Zaid Abdulla 4ad8aa5a1b Fixed bug in addObject task where a given parameter value may be set to all descendant parameters wth the same name. 2015-05-26 00:44:50 +03:00
Zaid Abdulla c70354997a Support nested parameters in object configurations in presets. 2015-05-25 11:22:53 +03:00
Zaid Abdulla c88e5d6fe8 Fixed bug causing only a single preset configuration of custom_command_age and custom_comand_value to take affect. 2015-02-01 19:59:53 +03:00
Zaid Abdulla 4e9186812c 1.0.3 2015-01-13 23:01:21 +03:00
Zaid Abdulla 41740cbf0b Fixed 404 error when deleting/retrying a task caused by recent change in node's implementation of querystring.unescape. 2014-11-22 19:11:41 +03:00
Zaid Abdulla 2932b9ab6d Correctly use https scheme for download RPC when FS_SSL config is set. Fixes #43. 2014-11-14 17:23:08 +03:00
Zaid Abdulla 3625f26134 1.0.2 2014-09-07 20:46:13 +03:00
Zaid Abdulla c1d486b776 Updated readme file. 2014-09-07 20:45:17 +03:00
Zaid Abdulla ae441beda0 1.0.1 2014-09-05 16:06:45 +03:00
Zaid Abdulla 6afe793c52 Fixed bug where refreshObject does not remove from database parameters that don't exist anymore in the device data model. 2014-09-05 16:01:47 +03:00
Zaid Abdulla 233daee88f Merge pull request #31 from akcoder/master
Fix SyntaxError on the inform message
2014-09-03 21:10:19 +03:00
Dan Morphis 91417a5c7e Fix parse error on the inform message 2014-09-02 10:14:05 -08:00
Zaid Abdulla 492679fb0e 1.0.0 2014-08-30 15:59:24 +03:00
Zaid Abdulla 141102908c Fixed device updates being processed twice potentially impacting performance. 2014-08-20 23:37:07 +03:00
Zaid Abdulla 7d3618a480 Fixed bug where deleting a device doesn't work if device ID contains URL encoded characters. 2014-08-18 22:23:46 +03:00
Zaid Abdulla b3ef66a581 Updated dependencies. 2014-08-16 14:45:24 +03:00
Zaid Abdulla faf1e49eb9 Implemented API for deleting a device. 2014-08-16 14:14:38 +03:00
Zaid Abdulla 76b67d9f54 Fixed crash in debug mode caused by incorrect debug directory path. 2014-08-16 09:23:14 +03:00
Zaid Abdulla 70b0dbad29 Added a config option to set Path attribute in Set-Cookie header. 2014-08-07 00:48:05 +03:00
Zaid Abdulla ee9a35b9b3 Ability to specify per device, per product class, or per oui configuration. Reimplemented Libxmljs options on the new configuration system. 2014-08-06 00:45:24 +03:00
Zaid Abdulla 3d201981e2 Use Node.js domains to gracefully handle exceptions. 2014-08-02 21:40:49 +03:00
Zaid Abdulla d5dc7b36df Flush redis after configuration changes that could affect cache integrity. 2014-08-01 22:30:03 +03:00
Zaid Abdulla d68b9d40ea Configuring XML parsing options readded after it was temporarily removed in recent config restructuring. 2014-08-01 13:17:34 +03:00
Zaid Abdulla 97700eb6c8 Use OUI instead of manufacturer to identify firmware files. 2014-08-01 00:47:01 +03:00
Zaid Abdulla c7a2cdb448 Fixed filename allowed character issues when uploading or deleting files through API. 2014-07-31 08:17:54 +03:00
Zaid Abdulla 32c56f8ff1 Accept configuration from environment variables or command line arguments. Unified cluster and HTTP server code for CWMP, NBI, and FS. 2014-07-31 01:44:53 +03:00
Zaid Abdulla 5d0f398bf7 Using a single connection URL to configure MongoDB connection. 2014-07-28 18:44:36 +03:00
Zaid Abdulla f49c7b1ad9 Fixed incorrect require() path in custom command samples. 2014-07-25 22:34:43 +03:00
Zaid Abdulla 4add20fe82 Support for TR-181 data model when looking for ConnectionRequestURL and SoftwareVersion parameters. 2014-07-25 22:33:12 +03:00
Zaid Abdulla 4c0c590791 Fixed bug where partial device update from a faulty task gets committed in the next inform. 2014-07-24 03:51:53 +03:00
Zaid Abdulla db6eae2aee Queue device updates and commit them using fewer database queries for better performance. Partial updates caused by faulty tasks will no longer be committed in order to avoid messing the device model. 2014-07-24 01:49:04 +03:00
Zaid Abdulla 4b244f43e0 Fixed issue where uploaded file not immediatly accessible because upload API responds before the upload has completed. 2014-07-19 06:58:48 +03:00
Zaid Abdulla 1c902af363 Wait for connections to MongoDB and redis to be established on startup. Added configuration option to specify redis DB. 2014-07-19 06:54:09 +03:00
Zaid Abdulla 28394af707 Added an option to configure the number of parameters per request in GetParameterValues and SetParameterValues methods. 2014-07-18 23:36:36 +03:00
Zaid Abdulla 47b0d06493 Crediting Netanel Rubin for his find. 2014-07-14 12:09:58 +03:00
Zaid Abdulla 03f9a9e628 Use RegExp objects instead of eval for creating regular expressions to fix code execution vulnerability as pointed out by Shahar Tal and his team from Check Point Software Technologies. 2014-07-14 10:43:02 +03:00
Zaid Abdulla a4f1d0dc2a Added copyright and permission header to source files. 2014-07-12 23:53:01 +03:00
Zaid Abdulla 2279de3c70 Fixed a couple of connection request issues. 2014-07-12 23:24:13 +03:00
Zaid Abdulla ec2dd87693 Set timeout value for TCP connetion based on CWMP session timeout. Handle expired sessions gracefully. 2014-07-10 01:08:54 +03:00
Zaid Abdulla bb6e963788 Added abilility to fetch parameter names iteratively to work around some devices' memory limitations. The configuration option GET_PARAMETER_NAMES_DEPTH_THRESHOLD specifies the depth of parameters upto which they're fetched iteratively. 2014-07-09 06:59:48 +03:00
Zaid Abdulla 3348139795 Moved detection of removed parameters to updateDevice function plus other organization and optimization changes 2014-07-07 23:32:42 +03:00
Zaid Abdulla b41e619a89 Storing session data in redis instead of cookies. Closes #10. Closes #20. 2014-06-30 02:35:22 +03:00
Zaid Abdulla 82b02ff175 Added config options to be passed to libxmljs when parsing request XML. Fixes #15. 2014-06-13 13:01:16 +03:00
Zaid Abdulla e725ba5be7 Fixed bug where fault response triggers method not supported exception when IGNORE_XML_NAMSPACES config option is enabled. 2014-06-12 23:52:38 +03:00
Zaid Abdulla 343520f408 Do not automatically close TCP connection at end of session. The CPE should, as described in the standard. Fixes 12. 2014-06-07 01:26:48 +03:00
33 changed files with 2034 additions and 997 deletions

No files matched your search

+1
View File
@@ -1,3 +1,4 @@
*~ *~
*.js *.js
node_modules node_modules
debug/*
+5 -6
View File
@@ -19,10 +19,10 @@ Install [Node.js](http://nodejs.org/), [Redis](http://redis.io/), and [MongoDB](
You may need to modify the configuration files under "config" directory (in /lib/node_modules/genieacs/config) depending on your setup. You may need to modify the configuration files under "config" directory (in /lib/node_modules/genieacs/config) depending on your setup.
Alternatively, you can use git to get the latest development version (not recommended for production use): Alternatively, you can install from source by cloning the git repository:
cd /opt cd /opt
git clone https://github.com/zaidka/genieacs.git git clone https://github.com/zaidka/genieacs.git --branch v1.0
cd genieacs cd genieacs
npm install npm install
npm run configure npm run configure
@@ -54,13 +54,12 @@ You may now proceed with installing [GenieACS GUI front end](https://github.com/
## Support ## Support
Documentation is still work in progress. Feel free to contact me if you require any assistance in installation, configuration, or using the APIs. The [Users mailing list](http://lists.genieacs.com) is a good place to get guidance and help from the community. Head on over and join the conversation! In addition, the [wiki](https://github.com/zaidka/genieacs/wiki) provides useful documentation and tips from GenieACS users.
You may submit bug reports or feature requests [here](https://github.com/zaidka/genieacs/issues). You may submit bug reports or feature requests [here](https://github.com/zaidka/genieacs/issues). For device interoperability issues, please consult the mailing list first — it's likely that a workaround already exists.
For commercial support options, please contact me. For commercial support options and professional services, please visit [genieacs.com](https://genieacs.com).
## Contributing ## Contributing
Contributions are welcome. Fork this repo and open a pull request and wait for feedback. You can also contribute by enhancing the documentation in the [wiki section](https://github.com/zaidka/genieacs/wiki). Contributions are welcome. Fork this repo and open a pull request and wait for feedback. You can also contribute by enhancing the documentation in the [wiki section](https://github.com/zaidka/genieacs/wiki).
+3 -4
View File
@@ -2,9 +2,8 @@
var path = require('path'); var path = require('path');
var fs = require('fs'); var fs = require('fs');
var dir = path.join(path.dirname(fs.realpathSync(__filename)), '../lib/'); var dir = path.resolve(path.dirname(fs.realpathSync(__filename)), '..');
process.title = 'genieacs-cwmp';
process.chdir(dir); process.chdir(dir);
require(path.join(dir, './cwmp')); cluster = require(path.resolve(dir, 'lib/cluster'));
cluster.start('cwmp');
+3 -4
View File
@@ -2,9 +2,8 @@
var path = require('path'); var path = require('path');
var fs = require('fs'); var fs = require('fs');
var dir = path.join(path.dirname(fs.realpathSync(__filename)), '../lib/'); var dir = path.resolve(path.dirname(fs.realpathSync(__filename)), '..');
process.title = 'genieacs-fs';
process.chdir(dir); process.chdir(dir);
require(path.join(dir, './fs')); cluster = require(path.resolve(dir, 'lib/cluster'));
cluster.start('fs');
+3 -4
View File
@@ -2,9 +2,8 @@
var path = require('path'); var path = require('path');
var fs = require('fs'); var fs = require('fs');
var dir = path.join(path.dirname(fs.realpathSync(__filename)), '../lib/'); var dir = path.resolve(path.dirname(fs.realpathSync(__filename)), '..');
process.title = 'genieacs-nbi';
process.chdir(dir); process.chdir(dir);
require(path.join(dir, './nbi')); cluster = require(path.resolve(dir, 'lib/cluster'));
cluster.start('nbi');
+12 -13
View File
@@ -1,16 +1,15 @@
{ {
"DATABASE_NAME" : "genieacs", "MONGODB_CONNECTION_URL" : "mongodb://127.0.0.1/genieacs",
"MONGODB_SOCKET" : "/tmp/mongodb-27017.sock", "REDIS_PORT" : "6379",
"REDIS_SOCKET" : "6379", "REDIS_HOST" : "127.0.0.1",
"ACS_INTERFACE" : "0.0.0.0", "CWMP_INTERFACE" : "0.0.0.0",
"ACS_PORT" : 7547, "CWMP_PORT" : 7547,
"ACS_HTTPS_INTERFACE" : "0.0.0.0", "CWMP_SSL" : false,
"ACS_HTTPS_PORT" : 7548, "NBI_INTERFACE" : "0.0.0.0",
"API_INTERFACE" : "0.0.0.0", "NBI_PORT" : 7557,
"API_PORT" : 7557, "FS_INTERFACE" : "0.0.0.0",
"FILES_INTERFACE" : "0.0.0.0", "FS_PORT" : 7567,
"FILES_PORT" : 7567, "FS_IP" : "192.168.0.1",
"FILES_IP" : "192.168.0.1",
"LOG_INFORMS" : true, "LOG_INFORMS" : true,
"DEBUG_DEVICES" : {} "DEBUG" : false
} }
@@ -1,3 +1,25 @@
/*
* Copyright 2013 Fanoos Telecom
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
var net = require('net'); var net = require('net');
var PORT = 23; var PORT = 23;
var USERNAME = 'mt7109'; var USERNAME = 'mt7109';
@@ -47,7 +69,7 @@ var telnetExecute = function(command, connection, prompt, callback) {
} }
var getDeviceIp = function(deviceId, callback) { var getDeviceIp = function(deviceId, callback) {
var db = require('../../db'); var db = require('../../lib/db');
var URL = require('url'); var URL = require('url');
db.devicesCollection.findOne({_id : deviceId}, {'InternetGatewayDevice.ManagementServer.ConnectionRequestURL._value' : 1}, function(err, device) { db.devicesCollection.findOne({_id : deviceId}, {'InternetGatewayDevice.ManagementServer.ConnectionRequestURL._value' : 1}, function(err, device) {
+23 -1
View File
@@ -1,3 +1,25 @@
/*
* Copyright 2013 Fanoos Telecom
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
// Change frequency plans for Seowon devices. // Change frequency plans for Seowon devices.
// Args format: 3532500 5000,3537500 5000,3542500 5000,3547500 5000,3552500 5000,3557500 5000,3505000 10000,3515000 10000,3525000 10000,3535000 10000,3545000 10000,3555000 10000 // Args format: 3532500 5000,3537500 5000,3542500 5000,3547500 5000,3552500 5000,3557500 5000,3505000 10000,3515000 10000,3525000 10000,3535000 10000,3545000 10000,3555000 10000
var net = require('net'); var net = require('net');
@@ -68,7 +90,7 @@ var findFrequency = function(haystack, needle) {
} }
var getDeviceIp = function(deviceId, callback) { var getDeviceIp = function(deviceId, callback) {
var db = require('../../db'); var db = require('../../lib/db');
var URL = require('url'); var URL = require('url');
db.devicesCollection.findOne({_id : deviceId}, {'InternetGatewayDevice.ManagementServer.ConnectionRequestURL._value' : 1}, function(err, device) { db.devicesCollection.findOne({_id : deviceId}, {'InternetGatewayDevice.ManagementServer.ConnectionRequestURL._value' : 1}, function(err, device) {
-17
View File
@@ -1,17 +0,0 @@
-----BEGIN CERTIFICATE-----
MIICqTCCAhICCQDSrolRw1qPBzANBgkqhkiG9w0BAQUFADCBmDELMAkGA1UEBhMC
SVExFTATBgNVBAgMDFN1bGF5bWFuaXlhaDEVMBMGA1UEBwwMU3VsYXltYW5peWFo
MQ8wDQYDVQQKDAZGYW5vb3MxEzARBgNVBAsMClRlY2hub2xvZ3kxDjAMBgNVBAMM
BWdlbmllMSUwIwYJKoZIhvcNAQkBFhZ6YWlkLmFiZHVsbGFAZmFub29zLmlxMB4X
DTEzMDIxNzEyNTYzN1oXDTE0MDIxNzEyNTYzN1owgZgxCzAJBgNVBAYTAklRMRUw
EwYDVQQIDAxTdWxheW1hbml5YWgxFTATBgNVBAcMDFN1bGF5bWFuaXlhaDEPMA0G
A1UECgwGRmFub29zMRMwEQYDVQQLDApUZWNobm9sb2d5MQ4wDAYDVQQDDAVnZW5p
ZTElMCMGCSqGSIb3DQEJARYWemFpZC5hYmR1bGxhQGZhbm9vcy5pcTCBnzANBgkq
hkiG9w0BAQEFAAOBjQAwgYkCgYEAxtHC3jlOoVsZQ9I8Wgd+8NkEaRfc01lRIAzj
eWuUO/xNY54EUDHecRxwolHtoGkkZAp4kKR3uUVdAo9uXmNT2CeeZ5lfRoa8WmgG
Yd6PExsdc4ZeKcw20XrKtQICqmU6rGlLERMFX97ejQh3eUmqwbzNaXiURxcKGdeK
2C/NxoUCAwEAATANBgkqhkiG9w0BAQUFAAOBgQAYFw6dy/z74ahnA5QpuJOjjTrt
AJaL9iuL1xS6TFfzne9UMIT+fFJsgSy6xx3TGkvbZJTnCzJN7I2uqmAhMoGFy7Ea
svCCkj8U5qkjqgwwi7IbwQ7fXxTVdPqUlkIdNB2YLDELksxCX+ZzGQPiFB8WrSoO
OBvB+hhPFpgp3aO2AQ==
-----END CERTIFICATE-----
-15
View File
@@ -1,15 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQDG0cLeOU6hWxlD0jxaB37w2QRpF9zTWVEgDON5a5Q7/E1jngRQ
Md5xHHCiUe2gaSRkCniQpHe5RV0Cj25eY1PYJ55nmV9GhrxaaAZh3o8TGx1zhl4p
zDbResq1AgKqZTqsaUsREwVf3t6NCHd5SarBvM1peJRHFwoZ14rYL83GhQIDAQAB
AoGAR4NpuW8ZhlZzNv7m8Gcj1NPqjzajGm5VDNc19xWoAw+UcpGC8amOXlcheHFJ
rNpE2asehMBzw/NcKjCXPWIy5LnGmttJMN2EbT2h/+uxBIM5IUc+joF744xF4C0t
5zrtqQYJxC7sBLX2vf7nY6iuovUM7fHEwACyV3Rpw3zdZf0CQQDiqW+ugLKcfS/c
6hkGTxeYWF4gUJTf3ke3au8nyMmUhyBwOuP4kpO72mK8bMLtjZRk3g6YChA4qH41
nfXUXIh7AkEA4I2/zj4GDjjspDR+/tX0iUHQk1Rm2CRmRXXFJV1f688OLODCcOFK
Ms/+qnbe41wXo1NUxCqVrnPwEh3MVl88/wJBANUTpXqtX7s+8ETECTxmE+9Se0Hz
ntCJXG/v5d1Ho0Cueu8mXXMrEBiTpygtMl4xL2RXX+7L3w30T5lF4nh9eIsCQQDY
DmsbdL/81TaTc9MxPQlxOzsFVrMxiixA7uTGY8ZULD/2XDeZxprOsyPUPlWMquIv
ipFHwbWNSg4WUEd4X2BfAkEAytze+0mCd0WCsKbKwfTQnmJ3bK7JQywtC03llWyA
Om3Vl61Syaa7M2hl5rLvitZMvU1Wd0s8raOXN8RUuRXZBw==
-----END RSA PRIVATE KEY-----
+9 -1
View File
@@ -8,12 +8,20 @@
"_deviceId._Manufacturer" : {"alias" : "summary.manufacturer"}, "_deviceId._Manufacturer" : {"alias" : "summary.manufacturer"},
"InternetGatewayDevice.DeviceInfo.HardwareVersion" : {"alias" : "summary.hardwareVersion"}, "InternetGatewayDevice.DeviceInfo.HardwareVersion" : {"alias" : "summary.hardwareVersion"},
"InternetGatewayDevice.DeviceInfo.SoftwareVersion" : {"alias" : "summary.softwareVersion"}, "InternetGatewayDevice.DeviceInfo.SoftwareVersion" : {"alias" : "summary.softwareVersion"},
"Device.DeviceInfo.HardwareVersion" : {"alias" : "summary.hardwareVersion"},
"Device.DeviceInfo.SoftwareVersion" : {"alias" : "summary.softwareVersion"},
"InternetGatewayDevice.WANDevice./^\\d+$/.WANConnectionDevice./^\\d+$/.WANIPConnection./^\\d+$/.MACAddress" : {"alias" : "summary.mac", "type" : "mac"}, "InternetGatewayDevice.WANDevice./^\\d+$/.WANConnectionDevice./^\\d+$/.WANIPConnection./^\\d+$/.MACAddress" : {"alias" : "summary.mac", "type" : "mac"},
"InternetGatewayDevice.WANDevice./^\\d+$/.WANConnectionDevice./^\\d+$/.WANIPConnection./^\\d+$/.ExternalIPAddress" : {"alias" : "summary.ip"}, "InternetGatewayDevice.WANDevice./^\\d+$/.WANConnectionDevice./^\\d+$/.WANIPConnection./^\\d+$/.ExternalIPAddress" : {"alias" : "summary.ip"},
"InternetGatewayDevice.WANDevice./^\\d+$/.WANConnectionDevice./^\\d+$/.WANPPPConnection./^\\d+$/.MACAddress" : {"alias" : "summary.mac", "type" : "mac"}, "InternetGatewayDevice.WANDevice./^\\d+$/.WANConnectionDevice./^\\d+$/.WANPPPConnection./^\\d+$/.MACAddress" : {"alias" : "summary.mac", "type" : "mac"},
"InternetGatewayDevice.WANDevice./^\\d+$/.WANConnectionDevice./^\\d+$/.WANPPPConnection./^\\d+$/.ExternalIPAddress" : {"alias" : "summary.ip"}, "InternetGatewayDevice.WANDevice./^\\d+$/.WANConnectionDevice./^\\d+$/.WANPPPConnection./^\\d+$/.ExternalIPAddress" : {"alias" : "summary.ip"},
"Device.WANDevice./^\\d+$/.WANConnectionDevice./^\\d+$/.WANIPConnection./^\\d+$/.MACAddress" : {"alias" : "summary.mac", "type" : "mac"},
"Device.WANDevice./^\\d+$/.WANConnectionDevice./^\\d+$/.WANIPConnection./^\\d+$/.ExternalIPAddress" : {"alias" : "summary.ip"},
"Device.WANDevice./^\\d+$/.WANConnectionDevice./^\\d+$/.WANPPPConnection./^\\d+$/.MACAddress" : {"alias" : "summary.mac", "type" : "mac"},
"Device.WANDevice./^\\d+$/.WANConnectionDevice./^\\d+$/.WANPPPConnection./^\\d+$/.ExternalIPAddress" : {"alias" : "summary.ip"},
"InternetGatewayDevice.LANDevice./^\\d+$/.WLANConfiguration./^\\d+$/.SSID" : {"alias" : "summary.wlanSsid"}, "InternetGatewayDevice.LANDevice./^\\d+$/.WLANConfiguration./^\\d+$/.SSID" : {"alias" : "summary.wlanSsid"},
"InternetGatewayDevice.LANDevice./^\\d+$/.WLANConfiguration./^\\d+$/.PreSharedKey.1.KeyPassphrase" : {"alias" : "summary.wlanPassphrase"} "InternetGatewayDevice.LANDevice./^\\d+$/.WLANConfiguration./^\\d+$/.PreSharedKey.1.KeyPassphrase" : {"alias" : "summary.wlanPassphrase"},
"Device.LANDevice./^\\d+$/.WLANConfiguration./^\\d+$/.SSID" : {"alias" : "summary.wlanSsid"},
"Device.LANDevice./^\\d+$/.WLANConfiguration./^\\d+$/.PreSharedKey.1.KeyPassphrase" : {"alias" : "summary.wlanPassphrase"}
} }
+90 -9
View File
@@ -1,3 +1,43 @@
###
# Copyright 2013, 2014 Zaid Abdulla
#
# 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/>.
#
# This file incorporates work covered by the following copyright and
# permission notice:
#
# Copyright 2013 Fanoos Telecom
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
###
config = require './config' config = require './config'
db = require './db' db = require './db'
http = require 'http' http = require 'http'
@@ -11,6 +51,17 @@ connectionRequest = (deviceId, callback) ->
# ensure socket is reused in case of digest authentication # ensure socket is reused in case of digest authentication
agent = new http.Agent({maxSockets : 1}) agent = new http.Agent({maxSockets : 1})
statusToError = (statusCode) ->
switch statusCode
when 200, 204
null
when 401
new Error('Incorrect connection request credentials')
when 0
new Error('Device is offline')
else
new Error("Unexpected response code from device: #{statusCode}")
conReq = (url, authString, callback) -> conReq = (url, authString, callback) ->
options = URL.parse(url) options = URL.parse(url)
options.agent = agent options.agent = agent
@@ -19,7 +70,7 @@ connectionRequest = (deviceId, callback) ->
options.headers = {'Authorization' : authString} options.headers = {'Authorization' : authString}
request = http.get(options, (res) -> request = http.get(options, (res) ->
if res.statusCode == 401 if res.statusCode == 401 and res.headers['www-authenticate']?
authHeader = auth.parseAuthHeader(res.headers['www-authenticate']) authHeader = auth.parseAuthHeader(res.headers['www-authenticate'])
callback(res.statusCode, authHeader) callback(res.statusCode, authHeader)
# don't need body, go ahead and emit events to free up the socket for possible reuse # don't need body, go ahead and emit events to free up the socket for possible reuse
@@ -39,11 +90,21 @@ connectionRequest = (deviceId, callback) ->
) )
) )
db.devicesCollection.findOne({_id : deviceId}, {'InternetGatewayDevice.ManagementServer.ConnectionRequestURL._value' : 1}, (err, device)-> proj = {
'Device.ManagementServer.ConnectionRequestURL._value' : 1,
'InternetGatewayDevice.ManagementServer.ConnectionRequestURL._value' : 1
}
db.devicesCollection.findOne({_id : deviceId}, proj, (err, device)->
if err if err
callback(err) callback(err)
return return
connectionRequestUrl = device.InternetGatewayDevice.ManagementServer.ConnectionRequestURL._value
if device.Device? # TR-181 data model
connectionRequestUrl = device.Device.ManagementServer.ConnectionRequestURL._value
else # TR-098 data model
connectionRequestUrl = device.InternetGatewayDevice.ManagementServer.ConnectionRequestURL._value
# for testing # for testing
#connectionRequestUrl = connectionRequestUrl.replace(/^(http:\/\/)([0-9\.]+)(\:[0-9]+\/[a-zA-Z0-9]+\/?$)/, '$110.1.1.254$3') #connectionRequestUrl = connectionRequestUrl.replace(/^(http:\/\/)([0-9\.]+)(\:[0-9]+\/[a-zA-Z0-9]+\/?$)/, '$110.1.1.254$3')
conReq(connectionRequestUrl, null, (statusCode, authHeader) -> conReq(connectionRequestUrl, null, (statusCode, authHeader) ->
@@ -56,25 +117,32 @@ connectionRequest = (deviceId, callback) ->
authString = auth.digest(username, password, uri.path, 'GET', null, authHeader) authString = auth.digest(username, password, uri.path, 'GET', null, authHeader)
conReq(connectionRequestUrl, authString, (statusCode, authHeader) -> conReq(connectionRequestUrl, authString, (statusCode, authHeader) ->
callback() if statusCode == 0
# Workaround for some devices unexpectedly closing the connection
return conReq(connectionRequestUrl, authString, (statusCode) -> callback(statusToError(statusCode)))
callback(statusToError(statusCode))
) )
else else
callback() callback(statusToError(statusCode))
) )
) )
watchTask = (taskId, timeout, callback) -> watchTask = (taskId, timeout, callback) ->
setTimeout( () -> setTimeout( () ->
db.tasksCollection.findOne({_id : taskId}, {'_id' : 1}, (err, task) -> db.tasksCollection.findOne({_id : taskId}, {'_id' : 1, 'fault' : 1}, (err, task) ->
return callback(err) if err
if task if task
timeout -= 500 timeout -= 500
if timeout < 0 if task.fault?
callback('timeout') callback(null, 'fault')
else if timeout <= 0
callback(null, 'timeout')
else else
watchTask(taskId, timeout, callback) watchTask(taskId, timeout, callback)
else else
callback(err) callback(null, 'completed')
) )
, 500) , 500)
@@ -169,8 +237,21 @@ insertTasks = (tasks, aliases, callback) ->
) )
deleteDevice = (deviceId, callback) ->
db.tasksCollection.remove({'device' : deviceId}, (err, removed) ->
return callback(err) if err
db.devicesCollection.remove({'_id' : deviceId}, (err, removed) ->
return callback(err) if err
db.redisClient.del("#{deviceId}_presets_hash", "#{deviceId}_inform_hash", (err) ->
callback(err)
)
)
)
exports.addAliases = addAliases exports.addAliases = addAliases
exports.sanitizeTask = sanitizeTask exports.sanitizeTask = sanitizeTask
exports.connectionRequest = connectionRequest exports.connectionRequest = connectionRequest
exports.watchTask = watchTask exports.watchTask = watchTask
exports.insertTasks = insertTasks exports.insertTasks = insertTasks
exports.deleteDevice = deleteDevice
+19 -2
View File
@@ -1,3 +1,20 @@
###
# Copyright 2013, 2014 Zaid Abdulla
#
# 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/>.
###
crypto = require 'crypto' crypto = require 'crypto'
@@ -7,11 +24,11 @@ exports.parseAuthHeader = (authHeader) ->
res['method'] = authHeader.slice(0, i) res['method'] = authHeader.slice(0, i)
options = authHeader.slice(i + 1) options = authHeader.slice(i + 1)
regex = /([a-z]+)="([a-zA-Z0-9\/\.@\-,= ]+)"/g regex = /([a-z]+)="([a-zA-Z0-9\/\.@\-\+,= ]+)"/g
while (r = regex.exec(options)) != null while (r = regex.exec(options)) != null
res[r[1]] = r[2] res[r[1]] = r[2]
regex = /([a-z]+)=([a-zA-Z0-9\/\.@\-]+)/g regex = /([a-z]+)=([a-zA-Z0-9\/\.@\-\+]+)/g
while (r = regex.exec(options)) != null while (r = regex.exec(options)) != null
res[r[1]] = r[2] res[r[1]] = r[2]
return res return res
+50
View File
@@ -0,0 +1,50 @@
###
# Copyright 2013, 2014 Zaid Abdulla
#
# 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/>.
###
config = require './config'
cluster = require 'cluster'
util = require 'util'
# Keep current configuration to pass that to workers as environment variables
ENVIRONMENT_VARIABLES = {}
for k, v of config.allConfig
ENVIRONMENT_VARIABLES["GENIEACS_#{k}"] = v
cluster.on('listening', (worker, address) ->
util.log("Worker #{worker.process.pid} listening to #{address.address}:#{address.port}")
)
cluster.on('exit', (worker, code, signal) ->
util.log("Worker #{worker.process.pid} died (#{worker.process.exitCode})")
cluster.fork(ENVIRONMENT_VARIABLES)
)
start = (service) ->
workerProcesses = config.get("#{service.toUpperCase()}_WORKER_PROCESSES")
cluster.setupMaster({
exec : 'lib/server',
args : [service]
})
for [0 ... workerProcesses]
cluster.fork(ENVIRONMENT_VARIABLES)
exports.start = start
+51 -3
View File
@@ -1,3 +1,28 @@
###
# Copyright 2013 Fanoos Telecom
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
###
buffer = require 'buffer'
querystring = require 'querystring'
UNDEFINED_TYPE = '[object Undefined]' UNDEFINED_TYPE = '[object Undefined]'
NULL_TYPE = '[object Null]' NULL_TYPE = '[object Null]'
BOOLEAN_TYPE = '[object Boolean]' BOOLEAN_TYPE = '[object Boolean]'
@@ -28,11 +53,11 @@ exports.arrayToHash = (arr) ->
return hash return hash
exports.getDeviceId = (deviceIdStruct) -> exports.generateDeviceId = (deviceIdStruct) ->
# Percent escaping function. Escapes everything except alphanumerics and underscore # Percent escaping function. Escapes everything except alphanumerics and underscore
esc = (str) -> esc = (str) ->
str.replace(/[^A-Za-z0-9_]/g, (chr) -> str.replace(/[^A-Za-z0-9_]/g, (chr) ->
buf = new require('buffer').Buffer(chr) buf = new buffer.Buffer(chr, 'utf8')
rep = '' rep = ''
rep += "%#{b.toString(16).toUpperCase()}" for b in buf rep += "%#{b.toString(16).toUpperCase()}" for b in buf
return rep return rep
@@ -45,11 +70,34 @@ exports.getDeviceId = (deviceIdStruct) ->
return "#{esc(deviceIdStruct['OUI'])}-#{esc(deviceIdStruct['SerialNumber'])}" return "#{esc(deviceIdStruct['OUI'])}-#{esc(deviceIdStruct['SerialNumber'])}"
exports.parseDeviceId = (deviceId) ->
parts = deviceId.split('-')
ret = {oui : querystring.unescape(parts[0])}
if parts.length == 3
ret.productClass = querystring.unescape(parts[1])
ret.serialNumber = querystring.unescape(parts[2])
else
ret.serialNumber = querystring.unescape(parts[1])
return ret
exports.extend = (obj, mixin) -> exports.extend = (obj, mixin) ->
obj[name] = method for name, method of mixin obj[name] = method for name, method of mixin
obj obj
exports.flattenObject = (object) ->
newObj = {}
f = (obj, prefix) ->
for k, v of obj
if typeof(v) is 'object'
f(v, "#{prefix}#{k}.")
else
newObj["#{prefix}#{k}"] = v
f(object, '')
return newObj
exports.getParamValueFromPath = (obj, path) -> exports.getParamValueFromPath = (obj, path) ->
pp = path.split('.') pp = path.split('.')
ref = obj ref = obj
+162 -36
View File
@@ -1,45 +1,171 @@
exports.DATABASE_NAME = 'genieacs' ###
exports.ACS_PORT = 7547 # CWMP port as assigned by IANA # Copyright 2013, 2014 Zaid Abdulla
exports.ACS_HTTPS_PORT = 7548 #
exports.ACS_INTERFACE = '0.0.0.0' # GenieACS is free software: you can redistribute it and/or modify
exports.ACS_HTTPS_INTERFACE = '0.0.0.0' # it under the terms of the GNU Affero General Public License as
exports.API_PORT = 7557 # published by the Free Software Foundation, either version 3 of the
exports.API_INTERFACE = '0.0.0.0' # License, or (at your option) any later version.
exports.FILES_PORT = 7567 #
exports.FILES_INTERFACE = '0.0.0.0' # GenieACS is distributed in the hope that it will be useful,
exports.FILES_IP = '172.240.21.2' # Used when sending download requests to devices # but WITHOUT ANY WARRANTY; without even the implied warranty of
exports.CACHE_DURATION = 60 # in seconds # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
exports.PRESETS_CACHE_DURATION = 86400 # GNU Affero General Public License for more details.
exports.PRESETS_TIME_PADDING = 1 #
exports.MONGODB_SOCKET = '/tmp/mongodb-27017.sock' # You should have received a copy of the GNU Affero General Public License
exports.REDIS_SOCKET = '/tmp/redis.sock' # along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
exports.WORKER_RESPAWN_TIME = 60000 ###
exports.LOG_INFORMS = false
exports.DEBUG_DEVICES = {} # {'202BC1-BM632w-8KA8WA1151100043' : true}
exports.DEVICE_ONLINE_THRESHOLD = 4000
exports.RETRY_DELAY = 300
exports.IGNORE_XML_NAMESPACES = false # Traverse XML using element's local name only
exports.MONGODB_OPTIONS = { path = require 'path'
db : { common = require './common'
w : 1
wtimeout : 60000 options = {
} CONFIG_DIR : {type : 'string', default : 'config'},
server : { MONGODB_CONNECTION_URL : {type : 'string', default : 'mongodb://127.0.0.1/genieacs'},
auto_reconnect : true REDIS_PORT : {type : 'int', default : 6379},
} REDIS_HOST : {type : 'string', default : '127.0.0.1'},
REDIS_DB : {type : 'int', default : 0},
CWMP_WORKER_PROCESSES : {type : 'int', default : 4},
CWMP_PORT : {type : 'int', default : 7547},
CWMP_INTERFACE : {type : 'string', default : '0.0.0.0'},
CWMP_SSL : {type : 'bool', default : false},
NBI_WORKER_PROCESSES : {type : 'int', default : 2},
NBI_PORT : {type : 'int', default : 7557},
NBI_INTERFACE : {type : 'string', default : '0.0.0.0'},
NBI_SSL : {type : 'bool', default : false},
FS_WORKER_PROCESSES : {type : 'int', default : 2},
FS_PORT : {type : 'int', default : 7567},
FS_INTERFACE : {type : 'string', default : '0.0.0.0'},
FS_SSL : {type : 'bool', default : false},
FS_IP : {type : 'string', default : '192.168.0.1'},
PRESETS_CACHE_DURATION : {type : 'int', default : 86400},
LOG_INFORMS : {type : 'bool', default : true},
DEBUG : {type : 'bool', default : false},
RETRY_DELAY : {type : 'int', default : 300},
IGNORE_XML_NAMESPACES : {type : 'bool', default : false},
SESSION_TIMEOUT : {type : 'int', default : 30},
GET_PARAMETER_NAMES_DEPTH_THRESHOLD : {type : 'int', default : 0},
TASK_PARAMETERS_BATCH_SIZE : {type : 'int', default : 32},
COOKIES_PATH : {type : 'string'},
# Libxml related configuration
XML_PARSE_RECOVER : {type : 'bool'},
XML_PARSE_NOENT : {type : 'bool'},
XML_PARSE_NOBLANKS : {type : 'bool'},
XML_PARSE_NSCLEAN : {type : 'bool'},
XML_PARSE_NOCDATA : {type : 'bool'},
XML_PARSE_IGNORE_ENC : {type : 'bool'},
# Should probably never be changed
PRESETS_TIME_PADDING : {type : 'int', default : 1},
DEVICE_ONLINE_THRESHOLD : {type : 'int', default : 4000}
} }
# load configuration allConfig = {}
c = require('../config/config')
for k, v of c
exports[k] = v setConfig = (name, value, commandLineArgument) ->
return true if allConfig[name]?
cast = (val, type) ->
switch type
when 'int'
Number(val)
when 'bool'
String(val).trim().toLowerCase() in ['true', 'on', 'yes', '1']
when 'string'
String(val)
else
null
_value = null
for optionName, optionDetails of options
n = optionName
if commandLineArgument
n = n.toLowerCase().replace(/_/g, '-')
if name == n
_value = cast(value, optionDetails.type)
n = optionName
else if common.startsWith(name, "#{n}-")
_value = cast(value, optionDetails.type)
n = "#{optionName}-#{name[optionName.length+1..]}"
if _value?
allConfig[n] = _value
# Save as environmnet variable to pass on to any child processes
process.env[n] = _value
return true
return false
# Command line arguments
exports.argv = []
argv = process.argv[2..]
while argv.length
arg = argv.shift()
if arg[0] == '-'
v = argv.shift()
exports.argv[arg] = v
setConfig(arg[2..], v, true)
else
exports.argv.push(arg)
# Environment variable
for k, v of process.env
continue if k.lastIndexOf('GENIEACS_', 0) != 0
k = k[9..] # remove "GENIEACS_" prefix
setConfig(k, v)
# Find config dir
if exports.argv['--config-dir']?
allConfig.CONFIG_DIR = exports.argv['--config-dir']
else if process.env['GENIEACS_CONFIG_DIR']?
allConfig.CONFIG_DIR = process.env['GENIEACS_CONFIG_DIR']
else
allConfig.CONFIG_DIR = options.CONFIG_DIR.default
# Configuration file
for k, v of require(path.resolve(allConfig.CONFIG_DIR, 'config'))
setConfig(k, v)
# Defaults
for k, v of options
setConfig(k, v.default) if v.default?
get = (option, deviceId) ->
if deviceId?
name = "#{option}-#{deviceId}"
v = allConfig[name]
return v if v?
i = name.lastIndexOf('-')
v = allConfig[name[0...i]]
return v if v?
i = name.lastIndexOf('-', i-1)
v = allConfig[name[0...i]]
return v if v? or i == -1
return allConfig[option]
# load parameter configurations # load parameter configurations
exports.PARAMETERS = require('../config/parameters') exports.PARAMETERS = require(path.resolve(allConfig.CONFIG_DIR, 'parameters'))
# load authentication scripts # load authentication scripts
exports.auth = require('../config/auth') exports.auth = require(path.resolve(allConfig.CONFIG_DIR, 'auth'))
exports.CUSTOM_COMMANDS = require('../config/custom_commands') exports.CUSTOM_COMMANDS = require(path.resolve(allConfig.CONFIG_DIR, 'custom_commands'))
exports.get = get
exports.allConfig = allConfig
+17
View File
@@ -1,3 +1,20 @@
###
# Copyright 2013, 2014 Zaid Abdulla
#
# 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/>.
###
config = require './config' config = require './config'
common = require './common' common = require './common'
+282 -316
View File
@@ -1,23 +1,56 @@
###
# Copyright 2013, 2014 Zaid Abdulla
#
# 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/>.
#
# This file incorporates work covered by the following copyright and
# permission notice:
#
# Copyright 2013 Fanoos Telecom
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
###
config = require './config' config = require './config'
common = require './common' common = require './common'
util = require 'util' util = require 'util'
http = require 'http'
https = require 'https'
soap = require './soap' soap = require './soap'
tasks = require './tasks' tasks = require './tasks'
normalize = require('./normalize').normalize
db = require './db' db = require './db'
presets = require './presets' presets = require './presets'
mongodb = require 'mongodb' mongodb = require 'mongodb'
fs = require 'fs'
apiFunctions = require './api-functions' apiFunctions = require './api-functions'
customCommands = require './custom-commands' customCommands = require './custom-commands'
crypto = require 'crypto' crypto = require 'crypto'
parameters = require './parameters'
zlib = require 'zlib' zlib = require 'zlib'
updateDevice = require './update-device'
OBJECT_REGEX = /\.$/
INSTANCE_REGEX = /\.[\d]+\.$/
# Used to reject new TR-069 sessions when under load # Used to reject new TR-069 sessions when under load
@@ -25,19 +58,14 @@ holdUntil = Date.now()
writeResponse = (currentRequest, res) -> writeResponse = (currentRequest, res) ->
data = new Buffer(res.data) if config.get('DEBUG', currentRequest.session.deviceId)
if data.length == 0 dump = "# RESPONSE #{new Date(Date.now())}\n" + JSON.stringify(res.headers) + "\n#{res.data}\n\n"
# no more requests. terminate TCP connection require('fs').appendFile("./debug/#{currentRequest.session.deviceId}.dump", dump, (err) ->
res.headers['Connection'] = 'close'
if config.DEBUG_DEVICES[currentRequest.deviceId]
dump = "# RESPONSE #{new Date(Date.now())}\n" + JSON.stringify(res.headers) + "\n#{data}\n\n"
fs = require('fs').appendFile("../debug/#{currentRequest.deviceId}.dump", dump, (err) ->
throw err if err throw err if err
) )
# respond using the same content-encoding as the request # respond using the same content-encoding as the request
if currentRequest.httpRequest.headers['content-encoding']? and data.length > 0 if currentRequest.httpRequest.headers['content-encoding']? and res.data.length > 0
switch currentRequest.httpRequest.headers['content-encoding'] switch currentRequest.httpRequest.headers['content-encoding']
when 'gzip' when 'gzip'
res.headers['Content-Encoding'] = 'gzip' res.headers['Content-Encoding'] = 'gzip'
@@ -47,97 +75,23 @@ writeResponse = (currentRequest, res) ->
compress = zlib.deflate compress = zlib.deflate
if compress? if compress?
compress(data, (err, data) -> compress(res.data, (err, data) ->
res.headers['Content-Length'] = data.length res.headers['Content-Length'] = data.length
currentRequest.httpResponse.writeHead(res.code, res.headers) currentRequest.httpResponse.writeHead(res.code, res.headers)
currentRequest.httpResponse.end(data) currentRequest.httpResponse.end(data)
) )
else else
res.headers['Content-Length'] = data.length res.headers['Content-Length'] = res.data.length
currentRequest.httpResponse.writeHead(res.code, res.headers) currentRequest.httpResponse.writeHead(res.code, res.headers)
currentRequest.httpResponse.end(data) currentRequest.httpResponse.end(res.data)
updateDevice = (currentRequest, actions, callback) -> endSession = (currentRequest) ->
if not actions? db.redisClient.del("session_#{currentRequest.sessionId}", (err, res) ->
callback() if callback? throw err if err
return res = soap.response(null)
writeResponse(currentRequest, res)
now = new Date(Date.now()) )
update = {'$set' : {}, '$unset' : {}}
if actions.parameterValues?
for p in actions.parameterValues
origValue = p[1]
v = normalize(p[0], origValue)
path = if common.endsWith(p[0], '.') then p[0] else "#{p[0]}."
if v == origValue
update['$unset']["#{path}_orig"] = 1
else
update['$set']["#{path}_orig"] = origValue
update['$set']["#{path}_value"] = v
update['$set']["#{path}_timestamp"] = now
if p[2]?
update['$set']["#{path}_type"] = p[2]
else
update['$unset']["#{path}_type"] = 1
if actions.deletedObjects?
for p in actions.deletedObjects
update['$unset'][p] = 1
if actions.instanceName?
for i in actions.instanceName
update['$set']["#{i[0]}._name"] = i[1]
if actions.parameterNames?
for p in actions.parameterNames
if OBJECT_REGEX.test(p[0])
path = p[0]
if INSTANCE_REGEX.test(p[0])
update['$set']["#{path}_instance"] = true
else
update['$set']["#{path}_object"] = true
else
path = "#{p[0]}."
update['$set']["#{path}_writable"] = p[1] if p[1]?
update['$set']["#{path}_timestamp"] = now
# Add parameter (sans any dot suffix) if has an alias
if (a = parameters.getAlias(pp = path.slice(0, -1)))?
newAliases = {} if not newAliases?
newAliases[pp] = a
if actions.customCommands?
for p in actions.customCommands
commandName = p[0]
update['$set']["_customCommands.#{commandName}._value"] = p[1]
update['$set']["_customCommands.#{commandName}._timestamp"] = now
if actions.set?
common.extend(update['$set'], actions.set)
for k of update
delete update[k] if Object.keys(update[k]).length == 0
if Object.keys(update).length
db.devicesCollection.update({'_id' : currentRequest.deviceId}, update, {}, (err, count) ->
throw err if err
# Clear aliases cache if there's a new aliased parameter that has not been included in aliases cache
if newAliases?
db.getAliases((aliases) ->
for p, a of newAliases
if not aliases[a]? or p not in aliases[a]
db.redisClient.del('aliases', (err, res) ->
throw err if err
)
break
)
callback?(err)
)
else
callback?()
inform = (currentRequest, cwmpRequest) -> inform = (currentRequest, cwmpRequest) ->
@@ -148,42 +102,49 @@ inform = (currentRequest, cwmpRequest) ->
now = new Date(Date.now()) now = new Date(Date.now())
util.log("#{currentRequest.deviceId}: Inform (#{cwmpRequest.methodRequest.event}); retry count #{cwmpRequest.methodRequest.retryCount}") if config.LOG_INFORMS if config.get('LOG_INFORMS', currentRequest.session.deviceId)
util.log("#{currentRequest.session.deviceId}: Inform (#{cwmpRequest.methodRequest.event}); retry count #{cwmpRequest.methodRequest.retryCount}")
parameterNames = (p[0] for p in cwmpRequest.methodRequest.parameterList) parameterNames = (p[0] for p in cwmpRequest.methodRequest.parameterList)
informHash = crypto.createHash('md5').update(JSON.stringify(parameterNames)).update(JSON.stringify(config.CUSTOM_COMMANDS)).digest('hex') informHash = crypto.createHash('md5').update(JSON.stringify(parameterNames)).update(JSON.stringify(config.CUSTOM_COMMANDS)).digest('hex')
actions = {parameterValues : cwmpRequest.methodRequest.parameterList, set : {}} deviceUpdates = {parameterValues : cwmpRequest.methodRequest.parameterList, informEvents : cwmpRequest.methodRequest.event}
actions.set._lastInform = now
actions.set._lastBoot = now if '1 BOOT' in cwmpRequest.methodRequest.event
# Delete all pending tasks on bootstrap (e.g. factory reset)
if '0 BOOTSTRAP' in cwmpRequest.methodRequest.event if '0 BOOTSTRAP' in cwmpRequest.methodRequest.event
actions.set._lastBootstrap = lastBootstrap = now lastBootstrap = now
# ignore all pending tasks db.tasksCollection.remove({device : currentRequest.session.deviceId}, (err, removed) ->
db.tasksCollection.remove({device : currentRequest.deviceId}, (err, removed) ->
throw err if err throw err if err
) )
db.redisClient.get("#{currentRequest.deviceId}_inform_hash", (err, oldInformHash) -> db.redisClient.get("#{currentRequest.session.deviceId}_inform_hash", (err, oldInformHash) ->
throw err if err throw err if err
updateAndRespond = () -> updateAndRespond = () ->
updateDevice(currentRequest, actions, (err) -> updateDevice.commitUpdates(currentRequest.session.deviceId, deviceUpdates, false, (err) ->
throw err if err throw err if err
res = soap.response({ db.redisClient.setex("session_#{currentRequest.sessionId}", currentRequest.session.sessionTimeout, JSON.stringify(currentRequest.session), (err) ->
id : cwmpRequest.id, throw err if err
methodResponse : {type : 'InformResponse'}, res = soap.response({
cwmpVersion : cwmpRequest.cwmpVersion, id : cwmpRequest.id,
cookies : {deviceId : currentRequest.deviceId} methodResponse : {type : 'InformResponse'},
}) cwmpVersion : cwmpRequest.cwmpVersion
writeResponse(currentRequest, res) })
if !!cookiesPath = config.get('COOKIES_PATH', currentRequest.session.deviceId)
res.headers['Set-Cookie'] = "session=#{currentRequest.sessionId}; Path=#{cookiesPath}"
else
res.headers['Set-Cookie'] = "session=#{currentRequest.sessionId}"
writeResponse(currentRequest, res)
)
) )
if oldInformHash == informHash and not lastBootstrap? if oldInformHash == informHash and not lastBootstrap?
return updateAndRespond() return updateAndRespond()
db.redisClient.setex("#{currentRequest.deviceId}_inform_hash", config.PRESETS_CACHE_DURATION, informHash, (err, res) -> db.redisClient.setex("#{currentRequest.session.deviceId}_inform_hash", config.get('PRESETS_CACHE_DURATION', currentRequest.session.deviceId), informHash, (err, res) ->
throw err if err throw err if err
) )
@@ -202,7 +163,7 @@ inform = (currentRequest, cwmpRequest) ->
path += p + '.' path += p + '.'
projection[path + '_timestamp'] = 1 projection[path + '_timestamp'] = 1
db.devicesCollection.findOne({'_id' : currentRequest.deviceId}, projection, (err, device) -> db.devicesCollection.findOne({'_id' : currentRequest.session.deviceId}, projection, (err, device) ->
throw err if err throw err if err
lastBootstrap ?= device?._lastBootstrap lastBootstrap ?= device?._lastBootstrap
_tasks = [] _tasks = []
@@ -212,7 +173,7 @@ inform = (currentRequest, cwmpRequest) ->
for k of reference for k of reference
continue if k[0] == '_' continue if k[0] == '_'
if not actual[k]?._timestamp? or actual[k]._timestamp < lastBootstrap if not actual[k]?._timestamp? or actual[k]._timestamp < lastBootstrap
_tasks.push({device: currentRequest.deviceId, name : 'refreshObject', objectName : reference[k]._path}) _tasks.push({device: currentRequest.session.deviceId, name : 'refreshObject', objectName : reference[k]._path, timestamp : now})
else else
traverse(reference[k], actual[k]) traverse(reference[k], actual[k])
@@ -223,18 +184,19 @@ inform = (currentRequest, cwmpRequest) ->
deviceCustomCommands = {} deviceCustomCommands = {}
deviceCustomCommands[k] = v._timestamp for k,v of device._customCommands deviceCustomCommands[k] = v._timestamp for k,v of device._customCommands
for cmd in customCommands.getDeviceCustomCommandNames(currentRequest.deviceId) for cmd in customCommands.getDeviceCustomCommandNames(currentRequest.session.deviceId)
if not (deviceCustomCommands[cmd]?._timestamp < lastBootstrap) if not (deviceCustomCommands[cmd]?._timestamp < lastBootstrap)
_tasks.push({device: currentRequest.deviceId, name: 'customCommand', command: "#{cmd} init"}) # Increment timestamp by one millisecond to ensure it runs after any refresh task
_tasks.push({device: currentRequest.session.deviceId, name: 'customCommand', command: "#{cmd} init", timestamp: new Date(now.getTime() + 1)})
delete deviceCustomCommands[cmd] delete deviceCustomCommands[cmd]
for cmd of deviceCustomCommands for cmd of deviceCustomCommands
actions.deletedObjects ?= [] deviceUpdates.deletedObjects ?= []
actions.deletedObjects.push("_customCommands.#{cmd}") deviceUpdates.deletedObjects.push("_customCommands.#{cmd}")
# clear presets hash if parameters are potentially modified # clear presets hash if parameters are potentially modified
if _tasks.length > 0 if _tasks.length > 0
db.redisClient.del("#{currentRequest.deviceId}_presets_hash", (err) -> db.redisClient.del("#{currentRequest.session.deviceId}_presets_hash", (err) ->
throw err if err throw err if err
) )
@@ -246,13 +208,14 @@ inform = (currentRequest, cwmpRequest) ->
for k, v of cwmpRequest.methodRequest.deviceId for k, v of cwmpRequest.methodRequest.deviceId
deviceIdDetails["_#{k}"] = v deviceIdDetails["_#{k}"] = v
db.devicesCollection.insert({_id : currentRequest.deviceId, _registered : now, _deviceId : deviceIdDetails}, (err) -> db.devicesCollection.insert({_id : currentRequest.session.deviceId, _registered : now, _deviceId : deviceIdDetails}, (err) ->
throw err if err throw err if err
util.log("#{currentRequest.deviceId}: New device registered") util.log("#{currentRequest.session.deviceId}: New device registered")
_tasks.push({device: currentRequest.deviceId, name : 'refreshObject', objectName : ''}) _tasks.push({device: currentRequest.session.deviceId, name : 'refreshObject', objectName : '', timestamp : now})
for cmd in customCommands.getDeviceCustomCommandNames(currentRequest.deviceId) for cmd in customCommands.getDeviceCustomCommandNames(currentRequest.session.deviceId)
_tasks.push({device: currentRequest.deviceId, name: 'customCommand', command: "#{cmd} init"}) # Increment timestamp by one millisecond to ensure it runs after any refresh task
_tasks.push({device: currentRequest.session.deviceId, name: 'customCommand', command: "#{cmd} init", timestamp: new Date(now.getTime() + 1)})
apiFunctions.insertTasks(_tasks, {}, () -> apiFunctions.insertTasks(_tasks, {}, () ->
updateAndRespond() updateAndRespond()
@@ -266,38 +229,43 @@ runTask = (currentRequest, task, methodResponse) ->
timeDiff = process.hrtime() timeDiff = process.hrtime()
tasks.task(task, methodResponse, (err, status, methodRequest, deviceUpdates) -> tasks.task(task, methodResponse, (err, status, methodRequest, deviceUpdates) ->
throw err if err throw err if err
# TODO handle error
updateDevice(currentRequest, deviceUpdates, (err) ->
throw err if err
timeDiff = process.hrtime(timeDiff)[0] + 1 timeDiff = process.hrtime(timeDiff)[0] + 1
if timeDiff > 3 # in seconds if timeDiff > 3 # in seconds
# Server under load. Hold new sessions temporarily. # Server under load. Hold new sessions temporarily.
holdUntil = Math.max(Date.now() + timeDiff * 2000, holdUntil) holdUntil = Math.max(Date.now() + timeDiff * 2000, holdUntil)
save = status & tasks.STATUS_SAVE save = status & tasks.STATUS_SAVE
switch status & ~tasks.STATUS_SAVE switch status & ~tasks.STATUS_SAVE
when tasks.STATUS_OK when tasks.STATUS_OK
f = () -> f = () ->
db.redisClient.setex(String(task._id), config.CACHE_DURATION, JSON.stringify(task), (err) -> db.redisClient.setex(String(task._id), currentRequest.session.sessionTimeout, JSON.stringify(task), (err) ->
throw err if err throw err if err
res = soap.response({ res = soap.response({
id : task._id, id : task._id,
methodRequest : methodRequest, methodRequest : methodRequest,
cwmpVersion : currentRequest.cwmpVersion cwmpVersion : currentRequest.session.cwmpVersion
}) })
writeResponse(currentRequest, res) writeResponse(currentRequest, res)
) updateSessionExpiry(currentRequest)
)
if save if save
updateDevice.commitUpdates(currentRequest.session.deviceId, deviceUpdates, true, (err) ->
throw err if err
db.tasksCollection.update({_id : mongodb.ObjectID(String(task._id))}, {$set : {session : task.session}}, (err) -> db.tasksCollection.update({_id : mongodb.ObjectID(String(task._id))}, {$set : {session : task.session}}, (err) ->
throw err if err throw err if err
f() f()
) )
else )
else
updateDevice.queueUpdates(currentRequest.session.deviceId, deviceUpdates, (err) ->
f() f()
when tasks.STATUS_COMPLETED )
util.log("#{currentRequest.deviceId}: Completed task #{task.name}(#{task._id})") when tasks.STATUS_COMPLETED
util.log("#{currentRequest.session.deviceId}: Completed task #{task.name}(#{task._id})")
updateDevice.commitUpdates(currentRequest.session.deviceId, deviceUpdates, true, (err) ->
throw err if err
db.tasksCollection.remove({'_id' : mongodb.ObjectID(String(task._id))}, (err, removed) -> db.tasksCollection.remove({'_id' : mongodb.ObjectID(String(task._id))}, (err, removed) ->
throw err if err throw err if err
db.redisClient.del(String(task._id), (err, res) -> db.redisClient.del(String(task._id), (err, res) ->
@@ -305,20 +273,20 @@ runTask = (currentRequest, task, methodResponse) ->
nextTask(currentRequest) nextTask(currentRequest)
) )
) )
when tasks.STATUS_FAULT )
retryAfter = config.RETRY_DELAY * Math.pow(2, task.retries ? 0) when tasks.STATUS_FAULT
util.log("#{currentRequest.deviceId}: Fault response for task #{task._id}. Retrying after #{retryAfter} seconds.") retryAfter = config.get('RETRY_DELAY', currentRequest.session.deviceId) * Math.pow(2, task.retries ? 0)
taskUpdate = {fault : task.fault, timestamp : new Date(Date.now() + retryAfter * 1000)} util.log("#{currentRequest.session.deviceId}: Fault response for task #{task._id}. Retrying after #{retryAfter} seconds.")
if save taskUpdate = {fault : task.fault, timestamp : new Date(Date.now() + retryAfter * 1000)}
taskUpdate.session = task.session if save
taskUpdate.session = task.session
db.tasksCollection.update({_id : mongodb.ObjectID(String(task._id))}, {$set : taskUpdate, $inc : {retries : 1}}, (err) -> db.tasksCollection.update({_id : mongodb.ObjectID(String(task._id))}, {$set : taskUpdate, $inc : {retries : 1}}, (err) ->
throw err if err throw err if err
nextTask(currentRequest) nextTask(currentRequest)
) )
else else
throw new Error('Unknown task status') throw new Error('Unknown task status')
)
) )
@@ -327,41 +295,41 @@ isTaskExpired = (task) ->
assertPresets = (currentRequest) -> assertPresets = (currentRequest) ->
db.redisClient.mget("#{currentRequest.deviceId}_presets_hash", 'presets_hash', (err, res) -> db.redisClient.mget("#{currentRequest.session.deviceId}_presets_hash", 'presets_hash', (err, res) ->
throw err if err throw err if err
devicePresetsHash = res[0] devicePresetsHash = res[0]
presetsHash = res[1] presetsHash = res[1]
if devicePresetsHash? and devicePresetsHash == presetsHash if devicePresetsHash? and devicePresetsHash == presetsHash
# no discrepancy, return empty response # no discrepancy, end the session
res = soap.response(null) endSession(currentRequest)
writeResponse(currentRequest, res)
else else
PRESETS_CACHE_DURATION = config.get('PRESETS_CACHE_DURATION', currentRequest.session.deviceId)
db.getPresetsObjectsAliases((allPresets, allObjects, allAliases) -> db.getPresetsObjectsAliases((allPresets, allObjects, allAliases) ->
if not presetsHash? if not presetsHash?
presetsHash = presets.calculatePresetsHash(allPresets, allObjects) presetsHash = presets.calculatePresetsHash(allPresets, allObjects)
db.redisClient.setex('presets_hash', config.PRESETS_CACHE_DURATION, presetsHash, (err, res) -> db.redisClient.setex('presets_hash', PRESETS_CACHE_DURATION, presetsHash, (err, res) ->
throw err if err throw err if err
) )
presets.getDevicePreset(currentRequest.deviceId, allPresets, allObjects, allAliases, (devicePreset) -> presets.getDevicePreset(currentRequest.session.deviceId, allPresets, allObjects, allAliases, (devicePreset) ->
presets.processDevicePreset(currentRequest.deviceId, devicePreset, (taskList, addTags, deleteTags, expiry) -> presets.processDevicePreset(currentRequest.session.deviceId, devicePreset, (taskList, addTags, deleteTags, expiry) ->
db.redisClient.setex("#{currentRequest.deviceId}_presets_hash", Math.floor(Math.max(1, expiry - config.PRESETS_TIME_PADDING)), presetsHash, (err, res) -> db.redisClient.setex("#{currentRequest.session.deviceId}_presets_hash", Math.floor(Math.max(1, expiry - config.get('PRESETS_TIME_PADDING'))), presetsHash, (err, res) ->
throw err if err throw err if err
) )
if addTags.length + deleteTags.length + taskList.length if addTags.length + deleteTags.length + taskList.length
util.log("#{currentRequest.deviceId}: Presets discrepancy found") util.log("#{currentRequest.session.deviceId}: Presets discrepancy found")
if addTags.length + deleteTags.length if addTags.length + deleteTags.length
util.log("#{currentRequest.deviceId}: Updating tags +(#{addTags}) -(#{deleteTags})") util.log("#{currentRequest.session.deviceId}: Updating tags +(#{addTags}) -(#{deleteTags})")
if deleteTags.length if deleteTags.length
db.devicesCollection.update({'_id' : currentRequest.deviceId}, {'$pull' : {'_tags' : {'$in' : deleteTags}}}, {}, (err, count) -> db.devicesCollection.update({'_id' : currentRequest.session.deviceId}, {'$pull' : {'_tags' : {'$in' : deleteTags}}}, {}, (err, count) ->
throw err if err throw err if err
) )
if addTags.length if addTags.length
db.devicesCollection.update({'_id' : currentRequest.deviceId}, {'$addToSet' : {'_tags' : {'$each' : addTags}}}, {}, (err, count) -> db.devicesCollection.update({'_id' : currentRequest.session.deviceId}, {'$addToSet' : {'_tags' : {'$each' : addTags}}}, {}, (err, count) ->
throw err if err throw err if err
) )
@@ -370,12 +338,11 @@ assertPresets = (currentRequest) ->
apiFunctions.insertTasks(taskList, allAliases, (err, taskList) -> apiFunctions.insertTasks(taskList, allAliases, (err, taskList) ->
throw err if err throw err if err
task = taskList[0] task = taskList[0]
util.log("#{currentRequest.deviceId}: Started task #{task.name}(#{task._id})") util.log("#{currentRequest.session.deviceId}: Started task #{task.name}(#{task._id})")
runTask(currentRequest, task, {}) runTask(currentRequest, task, {})
) )
else else
res = soap.response(null) endSession(currentRequest)
writeResponse(currentRequest, res)
) )
) )
) )
@@ -384,7 +351,7 @@ assertPresets = (currentRequest) ->
nextTask = (currentRequest) -> nextTask = (currentRequest) ->
now = new Date() now = new Date()
cur = db.tasksCollection.find({'device' : currentRequest.deviceId, timestamp : {$lte : now}}).sort(['timestamp']).limit(1) cur = db.tasksCollection.find({'device' : currentRequest.session.deviceId, timestamp : {$lte : now}}).sort(['timestamp']).limit(1)
cur.nextObject((err, task) -> cur.nextObject((err, task) ->
throw err if err throw err if err
@@ -392,14 +359,42 @@ nextTask = (currentRequest) ->
# no more tasks, check presets discrepancy # no more tasks, check presets discrepancy
assertPresets(currentRequest) assertPresets(currentRequest)
else if isTaskExpired(task) else if isTaskExpired(task)
util.log("#{currentRequest.deviceId}: Task is expired #{task.name}(#{task._id})") util.log("#{currentRequest.session.deviceId}: Task is expired #{task.name}(#{task._id})")
db.tasksCollection.remove({'_id' : mongodb.ObjectID(String(task._id))}, {safe: true}, (err, removed) -> db.tasksCollection.remove({'_id' : mongodb.ObjectID(String(task._id))}, {safe: true}, (err, removed) ->
throw err if err throw err if err
nextTask(currentRequest) nextTask(currentRequest)
) )
else else
util.log("#{currentRequest.deviceId}: Started task #{task.name}(#{task._id})") util.log("#{currentRequest.session.deviceId}: Started task #{task.name}(#{task._id})")
runTask(currentRequest, task, {}) updateDevice.clearUpdatesQueue(currentRequest.session.deviceId, (err) ->
throw err if err
runTask(currentRequest, task, {})
)
)
getSession = (httpRequest, callback) ->
# 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'
return callback() if not sessionId?
db.redisClient.get("session_#{sessionId}", (err, res) ->
throw err if err
return callback(sessionId, JSON.parse(res))
)
updateSessionExpiry = (currentRequest) ->
db.redisClient.expire("session_#{currentRequest.sessionId}", currentRequest.session.sessionTimeout, (err, res) ->
throw err if err
if not res
# Resave session in the very rare case when session cache expires while processing request
db.redisClient.setex("session_#{currentRequest.sessionId}", currentRequest.session.sessionTimeout, JSON.stringify(currentRequest.session), (err) ->
throw err if err
)
) )
@@ -424,14 +419,13 @@ listener = (httpRequest, httpResponse) ->
chunks = [] chunks = []
bytes = 0 bytes = 0
cookies = {}
stream.on('data', (chunk) -> stream.on('data', (chunk) ->
chunks.push(chunk) chunks.push(chunk)
bytes += chunk.length bytes += chunk.length
) )
httpRequest.getBody = (encoding) -> httpRequest.getBody = () ->
# Write all chunks into a Buffer # Write all chunks into a Buffer
body = new Buffer(bytes) body = new Buffer(bytes)
offset = 0 offset = 0
@@ -439,88 +433,92 @@ listener = (httpRequest, httpResponse) ->
chunk.copy(body, offset, 0, chunk.length) chunk.copy(body, offset, 0, chunk.length)
offset += chunk.length offset += chunk.length
) )
return body
#Return encoded (default to UTF8) string
return body.toString(encoding || 'utf8', 0, body.byteLength)
stream.on('end', () -> stream.on('end', () ->
cwmpRequest = soap.request(httpRequest) getSession(httpRequest, (sessionId, session) ->
cwmpRequest = soap.request(httpRequest, session?.cwmpVersion)
if not session?
if cwmpRequest.methodRequest?.type isnt 'Inform'
httpResponse.writeHead(400)
httpResponse.end('Session is expired')
return
currentRequest = { deviceId = common.generateDeviceId(cwmpRequest.methodRequest.deviceId)
httpRequest : httpRequest sessionId = crypto.randomBytes(8).toString('hex')
httpResponse : httpResponse
cwmpVersion : cwmpRequest.cwmpVersion
}
if cwmpRequest.methodRequest?.deviceId? session = {
currentRequest.deviceId = common.getDeviceId(cwmpRequest.methodRequest.deviceId) deviceId : deviceId,
else cwmpVersion : cwmpRequest.cwmpVersion,
currentRequest.deviceId = cwmpRequest.cookies.deviceId sessionTimeout : cwmpRequest.sessionTimeout ? config.get('SESSION_TIMEOUT', deviceId)
}
if config.DEBUG_DEVICES[currentRequest.deviceId] httpRequest.connection.setTimeout(session.sessionTimeout * 1000)
dump = "# REQUEST #{new Date(Date.now())}\n" + JSON.stringify(httpRequest.headers) + "\n#{httpRequest.getBody()}\n\n"
require('fs').appendFile("../debug/#{currentRequest.deviceId}.dump", dump, (err) ->
throw err if err
)
if cwmpRequest.methodRequest? currentRequest = {
if cwmpRequest.methodRequest.type is 'Inform' httpRequest : httpRequest,
inform(currentRequest, cwmpRequest) httpResponse : httpResponse,
else if cwmpRequest.methodRequest.type is 'GetRPCMethods' sessionId : sessionId,
util.log("#{currentRequest.deviceId}: GetRPCMethods") session : session
res = soap.response({ }
id : cwmpRequest.id,
methodResponse : {type : 'GetRPCMethodsResponse', methodList : ['Inform', 'GetRPCMethods', 'TransferComplete', 'RequestDownload']}, if config.get('DEBUG', currentRequest.session.deviceId)
cwmpVersion : currentRequest.cwmpVersion, dump = "# REQUEST #{new Date(Date.now())}\n" + JSON.stringify(httpRequest.headers) + "\n#{httpRequest.getBody()}\n\n"
cookies : cookies require('fs').appendFile("./debug/#{currentRequest.session.deviceId}.dump", dump, (err) ->
}) throw err if err
writeResponse(currentRequest, res) )
else if cwmpRequest.methodRequest.type is 'TransferComplete'
# do nothing if cwmpRequest.methodRequest?
util.log("#{currentRequest.deviceId}: Transfer complete") if cwmpRequest.methodRequest.type is 'Inform'
res = soap.response({ inform(currentRequest, cwmpRequest)
id : cwmpRequest.id, else if cwmpRequest.methodRequest.type is 'GetRPCMethods'
methodResponse : {type : 'TransferCompleteResponse'}, util.log("#{currentRequest.session.deviceId}: GetRPCMethods")
cwmpVersion : currentRequest.cwmpVersion,
cookies : cookies
})
writeResponse(currentRequest, res)
else if cwmpRequest.methodRequest.type is 'RequestDownload'
requestDownloadResponse = () ->
res = soap.response({ res = soap.response({
id : cwmpRequest.id, id : cwmpRequest.id,
methodResponse : {type : 'RequestDownloadResponse'}, methodResponse : {type : 'GetRPCMethodsResponse', methodList : ['Inform', 'GetRPCMethods', 'TransferComplete', 'RequestDownload']},
cwmpVersion : currentRequest.cwmpVersion, cwmpVersion : currentRequest.session.cwmpVersion
cookies : cookies
}) })
writeResponse(currentRequest, res) writeResponse(currentRequest, res)
fileType = cwmpRequest.methodRequest.fileType updateSessionExpiry(currentRequest)
util.log("#{currentRequest.deviceId}: RequestDownload (#{fileType})") else if cwmpRequest.methodRequest.type is 'TransferComplete'
if fileType isnt '1 Firmware Upgrade Image' # do nothing
# Only supporting firmware upgrade for now util.log("#{currentRequest.session.deviceId}: Transfer complete")
return requestDownloadResponse() res = soap.response({
id : cwmpRequest.id,
methodResponse : {type : 'TransferCompleteResponse'},
cwmpVersion : currentRequest.session.cwmpVersion
})
writeResponse(currentRequest, res)
updateSessionExpiry(currentRequest)
else if cwmpRequest.methodRequest.type is 'RequestDownload'
requestDownloadResponse = () ->
res = soap.response({
id : cwmpRequest.id,
methodResponse : {type : 'RequestDownloadResponse'},
cwmpVersion : currentRequest.session.cwmpVersion
})
writeResponse(currentRequest, res)
updateSessionExpiry(currentRequest)
fileType = cwmpRequest.methodRequest.fileType
util.log("#{currentRequest.session.deviceId}: RequestDownload (#{fileType})")
if fileType isnt '1 Firmware Upgrade Image'
# Only supporting firmware upgrade for now
return requestDownloadResponse()
db.getPresetsObjectsAliases((allPresets, allObjects, allAliases) -> db.getPresetsObjectsAliases((allPresets, allObjects, allAliases) ->
presets.getDevicePreset(currentRequest.deviceId, allPresets, allObjects, allAliases, (devicePreset) -> presets.getDevicePreset(currentRequest.session.deviceId, allPresets, allObjects, allAliases, (devicePreset) ->
presetSoftwareVersion = devicePreset.softwareVersion?.preset presetSoftwareVersion = devicePreset.softwareVersion?.preset
currentSoftwareVersion = devicePreset.softwareVersion?.current._value currentSoftwareVersion = devicePreset.softwareVersion?.current._value
if presetSoftwareVersion? and presetSoftwareVersion != currentSoftwareVersion if presetSoftwareVersion? and presetSoftwareVersion != currentSoftwareVersion
projection = { deviceIdStruct = common.parseDeviceId(currentRequest.session.deviceId)
'_deviceId._Manufacturer' : 1, db.filesCollection.findOne({'metadata.fileType' : '1 Firmware Upgrade Image', 'metadata.oui' : deviceIdStruct.oui, 'metadata.productClass' : deviceIdStruct.productClass, 'metadata.version' : presetSoftwareVersion}, {_id : 1}, (err, file) ->
'_deviceId._ProductClass' : 1,
}
db.devicesCollection.findOne({'_id' : currentRequest.deviceId}, projection, (err, device) ->
throw err if err
manufacturer = device._deviceId._Manufacturer
productClass = device._deviceId._ProductClass
db.filesCollection.findOne({'metadata.fileType' : '1 Firmware Upgrade Image', 'metadata.manufacturer' : manufacturer, 'metadata.productClass' : productClass, 'metadata.version' : presetSoftwareVersion}, {_id : 1}, (err, file) ->
throw err if err throw err if err
if not file? if not file?
util.error("#{currentRequest.deviceId}: Firmware image not found (#{presetSoftwareVersion})") util.error("#{currentRequest.session.deviceId}: Firmware image not found (#{presetSoftwareVersion})")
return requestDownloadResponse() return requestDownloadResponse()
task = { task = {
device : currentRequest.deviceId, device : currentRequest.session.deviceId,
name : 'download', name : 'download',
file : file['_id'] file : file['_id']
} }
@@ -529,72 +527,40 @@ listener = (httpRequest, httpResponse) ->
return requestDownloadResponse() return requestDownloadResponse()
) )
) )
) else
else return requestDownloadResponse()
return requestDownloadResponse() )
) )
else
throw new Error('ACS method not supported')
else if cwmpRequest.methodResponse?
taskId = cwmpRequest.id
db.getTask(taskId, (err, task) ->
throw err if err
if not task
nextTask(currentRequest)
else
runTask(currentRequest, task, cwmpRequest.methodResponse)
)
else if cwmpRequest.fault?
taskId = cwmpRequest.id
if not taskId
# Fault not related to a task. End the session.
return endSession(currentRequest)
db.getTask(taskId, (err, task) ->
throw err if err
if not task
nextTask(currentRequest)
else
runTask(currentRequest, task, cwmpRequest.fault)
) )
else else
throw new Error('ACS method not supported') # cpe sent empty response. start sending acs requests
else if cwmpRequest.methodResponse? nextTask(currentRequest)
taskId = cwmpRequest.id )
db.getTask(taskId, (err, task) ->
throw err if err
if not task
nextTask(currentRequest)
else
runTask(currentRequest, task, cwmpRequest.methodResponse)
)
else if cwmpRequest.fault?
taskId = cwmpRequest.id
if not taskId
# Fault not related to a task. return empty response.
res = soap.response(null)
writeResponse(currentRequest, res)
return
db.getTask(taskId, (err, task) ->
throw err if err
if not task
nextTask(currentRequest)
else
runTask(currentRequest, task, cwmpRequest.fault)
)
else
# cpe sent empty response. start sending acs requests
nextTask(currentRequest)
) )
cluster = require 'cluster'
numCPUs = require('os').cpus().length
if cluster.isMaster exports.listener = listener
db.redisClient.flushdb()
cluster.on('listening', (worker, address) ->
util.log("Worker #{worker.process.pid} listening to #{address.address}:#{address.port}")
)
cluster.on('exit', (worker, code, signal) ->
util.log("Worker #{worker.process.pid} died (#{worker.process.exitCode})")
setTimeout(()->
cluster.fork()
, config.WORKER_RESPAWN_TIME)
)
for [1 .. numCPUs]
cluster.fork()
else
options = {
key: fs.readFileSync('../config/httpscert.key'),
cert: fs.readFileSync('../config/httpscert.crt')
}
httpServer = http.createServer(listener)
httpsServer = https.createServer(options, listener)
# wait until DB connections are established
setTimeout(() ->
httpServer.listen(config.ACS_PORT, config.ACS_INTERFACE)
httpsServer.listen(config.ACS_HTTPS_PORT, config.ACS_HTTPS_INTERFACE)
, 1000)
+104 -29
View File
@@ -1,42 +1,116 @@
###
# Copyright 2013, 2014 Zaid Abdulla
#
# 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/>.
###
config = require './config' config = require './config'
mongodb = require 'mongodb' mongodb = require 'mongodb'
redisClient = require('redis').createClient(config.REDIS_SOCKET) redis = require('redis')
parameters = require './parameters' parameters = require './parameters'
redisClient = null
tasksCollection = null tasksCollection = null
devicesCollection = null devicesCollection = null
presetsCollection = null presetsCollection = null
objectsCollection = null objectsCollection = null
mongodb.MongoClient.connect("mongodb://#{config.MONGODB_SOCKET}/#{config.DATABASE_NAME}", config.MONGODB_OPTIONS, (err, db) ->
exports.mongoDb = db # Only flush redis DB if aliases or custom commands config has changed
db.collection('tasks', (err, collection) -> flushRedis = (callback) ->
throw new Error(err) if err? redisClient.get('config_hash', (err, res) ->
exports.tasksCollection = tasksCollection = collection return callback(err) if err
collection.ensureIndex({device: 1, timestamp: 1}, (err) ->
hash = require('crypto')
.createHash('md5')
.update(JSON.stringify(config.PARAMETERS))
.update(JSON.stringify(config.CUSTOM_COMMANDS))
.digest('hex')
if res == hash
return callback()
redisClient.flushdb((err) ->
return callback(err) if err
redisClient.set('config_hash', hash, (err, res) ->
return callback(err)
)
) )
) )
db.collection('devices', (err, collection) ->
throw new Error(err) if err? connect = (callback) ->
exports.devicesCollection = devicesCollection = collection callbackCounter = 6
mongodb.MongoClient.connect(config.get('MONGODB_CONNECTION_URL'), {db:{w:1},server:{autoReconnect:true}}, (err, db) ->
return callback(err) if err
exports.mongoDb = db
db.collection('tasks', (err, collection) ->
exports.tasksCollection = tasksCollection = collection
collection?.ensureIndex({device: 1, timestamp: 1}, (err) ->
)
if --callbackCounter == 0 or err
callbackCounter = 0
return callback(err)
)
db.collection('devices', (err, collection) ->
exports.devicesCollection = devicesCollection = collection
if --callbackCounter == 0 or err
callbackCounter = 0
return callback(err)
)
db.collection('presets', (err, collection) ->
exports.presetsCollection = presetsCollection = collection
if --callbackCounter == 0 or err
callbackCounter = 0
return callback(err)
)
db.collection('objects', (err, collection) ->
exports.objectsCollection = objectsCollection = collection
if --callbackCounter == 0 or err
callbackCounter = 0
return callback(err)
)
db.collection('fs.files', (err, collection) ->
exports.filesCollection = filesCollection = collection
if --callbackCounter == 0 or err
callbackCounter = 0
return callback(err)
)
exports.redisClient = redisClient = redis.createClient(config.get('REDIS_PORT'), config.get('REDIS_HOST'))
redisClient.select(config.get('REDIS_DB'), (err) ->
if err
callbackCounter = 0
return callback(err)
flushRedis((err) ->
if --callbackCounter == 0 or err
callbackCounter = 0
return callback(err)
)
)
) )
db.collection('presets', (err, collection) ->
throw new Error(err) if err?
exports.presetsCollection = presetsCollection = collection
)
db.collection('objects', (err, collection) -> disconnect = () ->
throw new Error(err) if err? exports.mongoDb.close()
exports.objectsCollection = objectsCollection = collection redisClient.quit()
)
db.collection('fs.files', (err, collection) ->
throw new Error(err) if err?
exports.filesCollection = filesCollection = collection
)
)
getTask = (taskId, callback) -> getTask = (taskId, callback) ->
@@ -100,7 +174,7 @@ getCached = (name, valueCallback, valueExpiry, callback) ->
getAliases = (callback) -> getAliases = (callback) ->
getCached('aliases', parameters.compileAliases, config.PRESETS_CACHE_DURATION, (err, res) -> getCached('aliases', parameters.compileAliases, config.get('PRESETS_CACHE_DURATION'), (err, res) ->
throw err if err throw err if err
callback(res) callback(res)
) )
@@ -120,7 +194,7 @@ getPresetsObjectsAliases = (callback) ->
presetsCollection.find().toArray((err, res) -> presetsCollection.find().toArray((err, res) ->
callback(err, res) callback(err, res)
) )
, config.PRESETS_CACHE_DURATION, (err, res) -> , config.get('PRESETS_CACHE_DURATION'), (err, res) ->
throw err if err throw err if err
presets = res presets = res
callback(presets, objects, aliases) if objects and aliases callback(presets, objects, aliases) if objects and aliases
@@ -134,14 +208,14 @@ getPresetsObjectsAliases = (callback) ->
objs[r._id] = r for r in res objs[r._id] = r for r in res
callback(null, objs) callback(null, objs)
) )
, config.PRESETS_CACHE_DURATION, (err, res) -> , config.get('PRESETS_CACHE_DURATION'), (err, res) ->
throw err if err throw err if err
objects = res objects = res
callback(presets, objects, aliases) if presets and aliases callback(presets, objects, aliases) if presets and aliases
) )
if not aliases if not aliases
getCached('aliases', parameters.compileAliases, config.PRESETS_CACHE_DURATION, (err, res) -> getCached('aliases', parameters.compileAliases, config.get('PRESETS_CACHE_DURATION'), (err, res) ->
throw err if err throw err if err
aliases = res aliases = res
callback(presets, objects, aliases) if presets and objects callback(presets, objects, aliases) if presets and objects
@@ -149,7 +223,8 @@ getPresetsObjectsAliases = (callback) ->
) )
exports.redisClient = redisClient
exports.getTask = getTask exports.getTask = getTask
exports.getPresetsObjectsAliases = getPresetsObjectsAliases exports.getPresetsObjectsAliases = getPresetsObjectsAliases
exports.getAliases = getAliases exports.getAliases = getAliases
exports.connect = connect
exports.disconnect = disconnect
+41 -43
View File
@@ -1,51 +1,49 @@
###
# Copyright 2013 Fanoos Telecom
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
###
config = require './config' config = require './config'
util = require 'util'
http = require 'http'
url = require 'url' url = require 'url'
mongodb = require 'mongodb' mongodb = require 'mongodb'
querystring = require 'querystring' querystring = require 'querystring'
db = require './db'
db = null
mongodb.MongoClient.connect("mongodb://#{config.MONGODB_SOCKET}/#{config.DATABASE_NAME}", config.MONGODB_OPTIONS, (err, _db) ->
db = _db
)
cluster = require 'cluster' listener = (request, response) ->
numCPUs = require('os').cpus().length urlParts = url.parse(request.url, true)
if request.method == 'GET'
filename = querystring.unescape(urlParts.pathname.substring(1))
gs = new mongodb.GridStore(db.mongoDb, filename, 'r', {})
gs.open((err, gs) ->
if err
response.writeHead(404)
response.end()
return
stream = gs.stream(true)
response.writeHead(200, {'Content-Type' : 'application/octet-stream', 'Content-Length' : gs.length})
stream.pipe(response)
)
else
response.writeHead(405, {'Allow': 'GET'})
response.end('405 Method Not Allowed')
if cluster.isMaster
cluster.on('listening', (worker, address) ->
util.log("Worker #{worker.process.pid} listening to #{address.address}:#{address.port}")
)
cluster.on('exit', (worker, code, signal) -> exports.listener = listener
util.log("Worker #{worker.process.pid} died (#{worker.process.exitCode})")
setTimeout(()->
cluster.fork()
, config.WORKER_RESPAWN_TIME)
)
for i in [1 .. numCPUs]
cluster.fork()
else
server = http.createServer((request, response) ->
urlParts = url.parse(request.url, true)
if request.method == 'GET'
filename = querystring.unescape(urlParts.pathname.substring(1))
gs = new mongodb.GridStore(db, filename, 'r', {})
gs.open((err, gs) ->
if err
response.writeHead(404)
response.end()
return
stream = gs.stream(true)
response.writeHead(200, {'Content-Type' : 'application/octet-stream', 'Content-Length' : gs.length})
stream.pipe(response)
)
else
response.writeHead(405, {'Allow': 'GET'})
response.end('405 Method Not Allowed')
)
server.listen config.FILES_PORT, config.FILES_INTERFACE
+41
View File
@@ -1,3 +1,43 @@
###
# Copyright 2013, 2014 Zaid Abdulla
#
# 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/>.
#
# This file incorporates work covered by the following copyright and
# permission notice:
#
# Copyright 2013 Fanoos Telecom
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
###
common = require './common' common = require './common'
@@ -93,6 +133,7 @@ optimizeProjection = (obj) ->
delete obj[b] delete obj[b]
keys.splice(i--, 1) keys.splice(i--, 1)
++ i ++ i
return
exports.test = test exports.test = test
+408 -312
View File
@@ -1,6 +1,44 @@
###
# Copyright 2013, 2014 Zaid Abdulla
#
# 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/>.
#
# This file incorporates work covered by the following copyright and
# permission notice:
#
# Copyright 2013 Fanoos Telecom
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
###
config = require './config' config = require './config'
util = require 'util'
http = require 'http'
url = require 'url' url = require 'url'
common = require './common' common = require './common'
db = require './db' db = require './db'
@@ -16,348 +54,406 @@ TASKS_REGEX = /^\/tasks\/([a-zA-Z0-9\-\_\%]+)(\/[a-zA-Z_]*)?$/
TAGS_REGEX = /^\/devices\/([a-zA-Z0-9\-\_\%]+)\/tags\/([a-zA-Z0-9\-\_\%]+)\/?$/ 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_]+s)\/?$/
DEVICE_PRESET_REGEX = /^\/devices\/([a-zA-Z0-9\-\_\%]+)\/preset\/?$/ DEVICE_PRESET_REGEX = /^\/devices\/([a-zA-Z0-9\-\_\%]+)\/preset\/?$/
DELETE_DEVICE_REGEX = /^\/devices\/([a-zA-Z0-9\-\_\%]+)\/?$/
cluster = require 'cluster' errorToString = (err) ->
numCPUs = require('os').cpus().length "#{err.name}: #{err.message}"
if cluster.isMaster
cluster.on('listening', (worker, address) ->
util.log("Worker #{worker.process.pid} listening to #{address.address}:#{address.port}")
)
cluster.on('exit', (worker, code, signal) -> listener = (request, response) ->
util.log("Worker #{worker.process.pid} died (#{worker.process.exitCode})") chunks = []
setTimeout(()-> bytes = 0
cluster.fork()
, config.WORKER_RESPAWN_TIME)
)
for [1 .. numCPUs] request.addListener 'data', (chunk) ->
cluster.fork() chunks.push(chunk)
else bytes += chunk.length
server = http.createServer (request, response) ->
chunks = []
bytes = 0
request.addListener 'data', (chunk) -> request.getBody = () ->
chunks.push(chunk) # Write all chunks into a Buffer
bytes += chunk.length body = new Buffer(bytes)
offset = 0
chunks.forEach((chunk) ->
chunk.copy(body, offset, 0, chunk.length)
offset += chunk.length
)
return body
request.getBody = (encoding) -> request.addListener 'end', () ->
# Write all chunks into a Buffer body = request.getBody()
body = new Buffer(bytes) urlParts = url.parse(request.url, true)
offset = 0 if PRESETS_REGEX.test(urlParts.pathname)
chunks.forEach((chunk) -> presetName = querystring.unescape(PRESETS_REGEX.exec(urlParts.pathname)[1])
chunk.copy(body, offset, 0, chunk.length) if request.method == 'PUT'
offset += chunk.length preset = JSON.parse(body)
) preset._id = presetName
# Return encoded (default to UTF8) string db.presetsCollection.save(preset, (err) ->
return body.toString(encoding || 'utf8', 0, body.byteLength) db.redisClient.del('presets', 'presets_hash', (err) ->
throw err if err
request.addListener 'end', () ->
body = request.getBody()
urlParts = url.parse(request.url, true)
if PRESETS_REGEX.test(urlParts.pathname)
presetName = querystring.unescape(PRESETS_REGEX.exec(urlParts.pathname)[1])
if request.method == 'PUT'
preset = JSON.parse(body)
preset._id = presetName
db.presetsCollection.save(preset, (err) ->
db.redisClient.del('presets', 'presets_hash', (err) ->
throw err if err
)
if err
response.writeHead(500)
response.end(err)
return
response.writeHead(200)
response.end()
) )
else if request.method == 'DELETE' if err
db.presetsCollection.remove({'_id' : presetName}, (err, removedCount) -> response.writeHead(500)
db.redisClient.del('presets', 'presets_hash', (err) -> response.end(errorToString(err))
throw err if err return
) response.writeHead(200)
if err response.end()
response.writeHead(500) )
response.end(err) else if request.method == 'DELETE'
return db.presetsCollection.remove({'_id' : presetName}, (err, removedCount) ->
response.writeHead(200) db.redisClient.del('presets', 'presets_hash', (err) ->
response.end() throw err if err
) )
else if err
response.writeHead 405, {'Allow': 'PUT, DELETE'} response.writeHead(500)
response.end('405 Method Not Allowed') response.end(errorToString(err))
else if OBJECTS_REGEX.test(urlParts.pathname) return
objectName = querystring.unescape(OBJECTS_REGEX.exec(urlParts.pathname)[1])
if request.method == 'PUT'
object = JSON.parse(body)
object._id = objectName
db.objectsCollection.save(object, (err) -> if removedCount == 0
db.redisClient.del('objects', 'presets_hash', (err) -> response.writeHead(404)
throw err if err
)
if err
response.writeHead(500)
response.end(err)
return
response.writeHead(200)
response.end() response.end()
) return
else if request.method == 'DELETE'
db.objectsCollection.remove({'_id' : objectName}, (err, removedCount) ->
db.redisClient.del('objects', 'presets_hash', (err) ->
throw err if err
)
if err
response.writeHead(500)
response.end(err)
return
response.writeHead(200)
response.end()
)
else
response.writeHead 405, {'Allow': 'PUT, DELETE'}
response.end('405 Method Not Allowed')
else if TAGS_REGEX.test(urlParts.pathname)
r = TAGS_REGEX.exec(urlParts.pathname)
deviceId = querystring.unescape(r[1])
tag = querystring.unescape(r[2])
if request.method == 'POST'
db.devicesCollection.update({'_id' : deviceId}, {'$addToSet' : {'_tags' : tag}}, {safe: true}, (err) ->
db.redisClient.del("#{deviceId}_presets_hash", (err) ->
throw err if err
)
if err
response.writeHead(500)
response.end(err)
return
response.writeHead(200)
response.end()
)
else if request.method == 'DELETE'
db.devicesCollection.update({'_id' : deviceId}, {'$pull' : {'_tags' : tag}}, {safe: true}, (err) ->
db.redisClient.del("#{deviceId}_presets_hash", (err) ->
throw err if err
)
if err
response.writeHead(500)
response.end(err)
return
response.writeHead(200)
response.end()
)
else
response.writeHead 405, {'Allow': 'POST, DELETE'}
response.end('405 Method Not Allowed')
else if DEVICE_TASKS_REGEX.test(urlParts.pathname)
if request.method == 'POST'
deviceId = querystring.unescape(DEVICE_TASKS_REGEX.exec(urlParts.pathname)[1])
if body
task = JSON.parse(body)
task.device = deviceId
db.getAliases((aliases) ->
apiFunctions.insertTasks(task, aliases, (err) ->
db.redisClient.del("#{deviceId}_presets_hash", (err) ->
throw err if err
)
if err
response.writeHead(500)
response.end(err)
return
if urlParts.query.connection_request? response.writeHead(200)
apiFunctions.connectionRequest(deviceId, (err) -> response.end()
if err )
response.writeHead(202) else
response.end() response.writeHead 405, {'Allow': 'PUT, DELETE'}
else response.end('405 Method Not Allowed')
apiFunctions.watchTask(task._id, config.DEVICE_ONLINE_THRESHOLD, (err) -> else if OBJECTS_REGEX.test(urlParts.pathname)
if err objectName = querystring.unescape(OBJECTS_REGEX.exec(urlParts.pathname)[1])
response.writeHead(202) if request.method == 'PUT'
response.end() object = JSON.parse(body)
return object._id = objectName
response.writeHead(200)
response.end() db.objectsCollection.save(object, (err) ->
) db.redisClient.del('objects', 'presets_hash', (err) ->
) throw err if err
else )
response.writeHead(202) if err
response.end() response.writeHead(500)
response.end(errorToString(err))
return
response.writeHead(200)
response.end()
)
else if request.method == 'DELETE'
db.objectsCollection.remove({'_id' : objectName}, (err, removedCount) ->
db.redisClient.del('objects', 'presets_hash', (err) ->
throw err if err
)
if err
response.writeHead(500)
response.end(errorToString(err))
return
if removedCount == 0
response.writeHead(404)
response.end()
return
response.writeHead(200)
response.end()
)
else
response.writeHead 405, {'Allow': 'PUT, DELETE'}
response.end('405 Method Not Allowed')
else if TAGS_REGEX.test(urlParts.pathname)
r = TAGS_REGEX.exec(urlParts.pathname)
deviceId = querystring.unescape(r[1])
tag = querystring.unescape(r[2])
if request.method == 'POST'
db.devicesCollection.update({'_id' : deviceId}, {'$addToSet' : {'_tags' : tag}}, {safe: true}, (err, updatedCount) ->
db.redisClient.del("#{deviceId}_presets_hash", (err) ->
throw err if err
)
if err
response.writeHead(500)
response.end(errorToString(err))
return
if updatedCount == 0
response.writeHead(404)
response.end()
return
response.writeHead(200)
response.end()
)
else if request.method == 'DELETE'
db.devicesCollection.update({'_id' : deviceId}, {'$pull' : {'_tags' : tag}}, {safe: true}, (err, updatedCount) ->
db.redisClient.del("#{deviceId}_presets_hash", (err) ->
throw err if err
)
if err
response.writeHead(500)
response.end(errorToString(err))
return
if updatedCount == 0
response.writeHead(404)
response.end()
return
response.writeHead(200)
response.end()
)
else
response.writeHead 405, {'Allow': 'POST, DELETE'}
response.end('405 Method Not Allowed')
else if DEVICE_TASKS_REGEX.test(urlParts.pathname)
if request.method == 'POST'
deviceId = querystring.unescape(DEVICE_TASKS_REGEX.exec(urlParts.pathname)[1])
if body.length
task = JSON.parse(body)
task.device = deviceId
db.getAliases((aliases) ->
apiFunctions.insertTasks(task, aliases, (err) ->
db.redisClient.del("#{deviceId}_presets_hash", (err) ->
throw err if err
) )
)
else if urlParts.query.connection_request?
# no task, send connection request only
apiFunctions.connectionRequest(deviceId, (err) ->
if err if err
response.writeHead 504 response.writeHead(500)
response.end() response.end(errorToString(err))
return return
response.writeHead 200
response.end() if urlParts.query.connection_request?
apiFunctions.connectionRequest(deviceId, (err) ->
if err
response.writeHead(202, err.message, {'Content-Type' : 'application/json'})
response.end(JSON.stringify(task))
else
apiFunctions.watchTask(task._id, config.get('DEVICE_ONLINE_THRESHOLD', deviceId), (err, status) ->
if err
response.writeHead(500)
response.end(errorToString(err))
return
if status is 'timeout'
response.writeHead(202, 'Task queued but not processed', {'Content-Type' : 'application/json'})
response.end(JSON.stringify(task))
else if status is 'fault'
db.tasksCollection.findOne({_id : task._id}, (err, task) ->
if err
response.writeHead(500)
response.end(errorToString(err))
return
response.writeHead(202, 'Task faulted', {'Content-Type' : 'application/json'})
response.end(JSON.stringify(task))
)
else
response.writeHead(200, {'Content-Type' : 'application/json'})
response.end(JSON.stringify(task))
)
)
else
response.writeHead(202, {'Content-Type' : 'application/json'})
response.end(JSON.stringify(task))
) )
else )
response.writeHead(400) else if urlParts.query.connection_request?
# no task, send connection request only
apiFunctions.connectionRequest(deviceId, (err) ->
if err
response.writeHead 504
response.end(errorToString(err))
return
response.writeHead 200
response.end() response.end()
)
else
response.writeHead(400)
response.end()
else
response.writeHead 405, {'Allow': 'POST'}
response.end('405 Method Not Allowed')
else if DEVICE_PRESET_REGEX.test(urlParts.pathname)
deviceId = querystring.unescape(DEVICE_PRESET_REGEX.exec(urlParts.pathname)[1])
if request.method is 'GET'
db.getPresetsObjectsAliases((allPresets, allObjects, allAliases) ->
presets.getDevicePreset(deviceId, allPresets, allObjects, allAliases, (devicePreset) ->
response.writeHead(200, {'Content-Type' : 'application/json'})
response.end(JSON.stringify(devicePreset))
)
)
else
response.writeHead 405, {'Allow': 'GET'}
response.end('405 Method Not Allowed')
else if TASKS_REGEX.test(urlParts.pathname)
r = TASKS_REGEX.exec(urlParts.pathname)
taskId = mongodb.ObjectID(querystring.unescape(r[1]))
action = r[2]
if not action? or action is '/'
if request.method == 'DELETE'
db.tasksCollection.remove({'_id' : taskId}, (err, removedCount) ->
if err
response.writeHead(500)
response.end(errorToString(err))
return
if removedCount == 0
response.writeHead(404)
response.end()
return
response.writeHead(200)
response.end()
)
else
response.writeHead 405, {'Allow': 'PUT DELETE'}
response.end('405 Method Not Allowed')
else if action is '/retry'
if request.method == 'POST'
db.tasksCollection.update({_id : taskId}, {$unset : {fault : 1}, $set : {timestamp : new Date()}}, (err, updatedCount) ->
# TODO need to invalidate presets hash for the device
throw err if err
if updatedCount == 0
response.writeHead(404)
response.end()
return
response.writeHead(200)
response.end()
)
else else
response.writeHead 405, {'Allow': 'POST'} response.writeHead 405, {'Allow': 'POST'}
response.end('405 Method Not Allowed') response.end('405 Method Not Allowed')
else if DEVICE_PRESET_REGEX.test(urlParts.pathname) else
deviceId = querystring.unescape(DEVICE_PRESET_REGEX.exec(urlParts.pathname)[1]) response.writeHead(404)
if request.method is 'GET' response.end()
db.getPresetsObjectsAliases((allPresets, allObjects, allAliases) -> else if FILES_REGEX.test(urlParts.pathname)
presets.getDevicePreset(deviceId, allPresets, allObjects, allAliases, (devicePreset) -> filename = querystring.unescape(FILES_REGEX.exec(urlParts.pathname)[1])
response.writeHead(200, {'Content-Type' : 'application/json'}) if request.method == 'PUT'
response.end(JSON.stringify(devicePreset)) metadata = {
) fileType : request.headers.filetype,
) oui : request.headers.oui,
else productClass : request.headers.productclass,
response.writeHead 405, {'Allow': 'GET'} version : request.headers.version,
response.end('405 Method Not Allowed') }
else if TASKS_REGEX.test(urlParts.pathname)
r = TASKS_REGEX.exec(urlParts.pathname)
taskId = mongodb.ObjectID(querystring.unescape(r[1]))
action = try querystring.unescape(r[2])
if not action? or action is '/'
if request.method == 'DELETE'
db.tasksCollection.remove({'_id' : taskId}, (err, removedCount) ->
if err
response.writeHead(500)
response.end(err)
return
response.writeHead(200)
response.end()
)
else
response.writeHead 405, {'Allow': 'PUT DELETE'}
response.end('405 Method Not Allowed')
else if action is '/retry'
if request.method == 'POST'
db.tasksCollection.update({_id : taskId}, {$unset : {fault : 1}, $set : {timestamp : new Date()}}, (err, count) ->
# TODO need to invalidate presets hash for the device
response.writeHead(200)
response.end()
)
else
response.writeHead 405, {'Allow': 'POST'}
response.end('405 Method Not Allowed')
else
response.writeHead(404)
response.end()
else if FILES_REGEX.test(urlParts.pathname)
filename = querystring.unescape(FILES_REGEX.exec(urlParts.pathname)[1])
if request.method == 'PUT'
metadata = {
fileType : request.headers.filetype,
manufacturer : request.headers.manufacturer,
productClass : request.headers.productclass,
version : request.headers.version,
}
gs = new mongodb.GridStore(db.mongoDb, filename, 'w', {metadata : metadata}) gs = new mongodb.GridStore(db.mongoDb, filename, 'w', {metadata : metadata})
gs.open((err, gs) -> gs.open((err, gs) ->
gs.write(request.getBody('binary'), (err, res) -> gs.write(body, (err, res) ->
throw err if err
gs.close((err) ->
throw err if err throw err if err
gs.close((err) ->
throw err if err
)
response.writeHead(201) response.writeHead(201)
response.end() response.end()
) )
) )
else if request.method == 'DELETE'
mongodb.GridStore.unlink(db.mongoDb, filename, (err) ->
response.writeHead(200)
response.end()
)
else
response.writeHead 405, {'Allow': 'PUT, DELETE'}
response.end('405 Method Not Allowed')
else if PING_REGEX.test(urlParts.pathname)
host = querystring.unescape(PING_REGEX.exec(urlParts.pathname)[1])
require('child_process').exec("ping -w 1 -i 0.2 -c 3 #{host}", (err, stdout, stderr) ->
if err?
response.writeHead(404, {'Cache-Control' : 'no-cache'})
response.end()
return
response.writeHead(200, {'Content-Type' : 'text/plain', 'Cache-Control' : 'no-cache'})
response.end(stdout)
) )
else if QUERY_REGEX.test(urlParts.pathname) else if request.method == 'DELETE'
collectionName = QUERY_REGEX.exec(urlParts.pathname)[1] mongodb.GridStore.unlink(db.mongoDb, filename, (err) ->
if request.method not in ['GET', 'HEAD'] response.writeHead(200)
response.writeHead 405, {'Allow' : 'GET, HEAD'} response.end()
response.end('405 Method Not Allowed') )
return
collection = db["#{collectionName}Collection"]
if not collection?
response.writeHead 404
response.end('404 Not Found')
return
func = (aliases) ->
if urlParts.query.query?
q = JSON.parse(urlParts.query.query)
else
q = {}
q = query.expand(q, aliases) if collectionName is 'devices'
if urlParts.query.projection?
projection = {}
for p in urlParts.query.projection.split(',')
p = p.trim()
projection[p] = 1
if collectionName is 'devices'
for k,v of aliases
if k == p or common.startsWith(k, "#{p}.")
projection[a] = 1 for a in v
cur = collection.find(q, projection, {batchSize : 50})
if urlParts.query.sort?
s = JSON.parse(urlParts.query.sort)
sort = {}
for k,v of s
if aliases[k]?
sort[a] = v for a in aliases[k]
else
sort[k] = v
cur.sort(sort)
cur.skip(parseInt(urlParts.query.skip)) if urlParts.query.skip?
cur.limit(parseInt(urlParts.query.limit)) if urlParts.query.limit?
cur.count((err, total) ->
response.writeHead(200, {'Content-Type' : 'application/json', 'total' : total})
if request.method is 'HEAD'
response.end()
return
response.write("[\n")
i = 0
cur.each((err, item) ->
if item is null
response.end("\n]")
else
response.write(",\n") if i++
apiFunctions.addAliases(item, aliases) if collectionName is 'devices'
response.write(JSON.stringify(item))
)
)
if collectionName is 'devices'
db.getAliases((aliases) ->
func(aliases)
)
else
func({})
else else
response.writeHead 405, {'Allow': 'PUT, DELETE'}
response.end('405 Method Not Allowed')
else if PING_REGEX.test(urlParts.pathname)
host = querystring.unescape(PING_REGEX.exec(urlParts.pathname)[1])
require('child_process').exec("ping -w 1 -i 0.2 -c 3 #{host}", (err, stdout, stderr) ->
if err
response.writeHead(404, {'Cache-Control' : 'no-cache'})
response.end(errorToString(err))
return
response.writeHead(200, {'Content-Type' : 'text/plain', 'Cache-Control' : 'no-cache'})
response.end(stdout)
)
else if DELETE_DEVICE_REGEX.test(urlParts.pathname)
if request.method isnt 'DELETE'
response.writeHead(405, {'Allow' : 'DELETE'})
response.end('405 Method Not Allowed')
return
deviceId = querystring.unescape(DELETE_DEVICE_REGEX.exec(urlParts.pathname)[1])
apiFunctions.deleteDevice(deviceId, (err) ->
if err
response.writeHead(500)
response.end(errorToString(err))
return
response.writeHead(200)
response.end()
)
else if QUERY_REGEX.test(urlParts.pathname)
collectionName = QUERY_REGEX.exec(urlParts.pathname)[1]
if request.method not in ['GET', 'HEAD']
response.writeHead 405, {'Allow' : 'GET, HEAD'}
response.end('405 Method Not Allowed')
return
collection = db["#{collectionName}Collection"]
if not collection?
response.writeHead 404 response.writeHead 404
response.end('404 Not Found') response.end('404 Not Found')
return
server.listen config.API_PORT, config.API_INTERFACE func = (aliases) ->
if urlParts.query.query?
try
q = JSON.parse(urlParts.query.query)
catch err
response.writeHead(400)
response.end(errorToString(err))
return
else
q = {}
q = query.expand(q, aliases) if collectionName is 'devices'
q = query.substituteObjectId(q) if collectionName is 'tasks'
if urlParts.query.projection?
projection = {}
for p in urlParts.query.projection.split(',')
p = p.trim()
projection[p] = 1
if collectionName is 'devices'
for k,v of aliases
if k == p or common.startsWith(k, "#{p}.")
projection[a] = 1 for a in v
cur = collection.find(q, projection, {batchSize : 50})
if urlParts.query.sort?
s = JSON.parse(urlParts.query.sort)
sort = {}
for k,v of s
if aliases[k]?
sort[a] = v for a in aliases[k]
else
sort[k] = v
cur.sort(sort)
cur.skip(parseInt(urlParts.query.skip)) if urlParts.query.skip?
cur.limit(parseInt(urlParts.query.limit)) if urlParts.query.limit?
cur.count((err, total) ->
response.writeHead(200, {'Content-Type' : 'application/json', 'total' : total})
if request.method is 'HEAD'
response.end()
return
response.write("[\n")
i = 0
cur.each((err, item) ->
if item is null
response.end("\n]")
else
response.write(",\n") if i++
apiFunctions.addAliases(item, aliases) if collectionName is 'devices'
response.write(JSON.stringify(item))
)
)
if collectionName is 'devices'
db.getAliases((aliases) ->
func(aliases)
)
else
func({})
else
response.writeHead 404
response.end('404 Not Found')
exports.listener = listener
+35 -12
View File
@@ -1,27 +1,50 @@
###
# Copyright 2013 Fanoos Telecom
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
###
# Credit to Netanel Rubin from Check Point Software Technologies for discovering
# vulnerability caused by use of eval to generate regex from user input.
common = require './common' common = require './common'
parameters = require './parameters' parameters = require './parameters'
stringToRegexp = (input) -> stringToRegexp = (input, flags) ->
if (input.indexOf('*') == -1) if (input.indexOf('*') == -1)
return false return false
output = input.replace(/[\[\]\\\^\$\.\|\?\+\(\)]/, "\\$&") output = input.replace(/[\[\]\\\^\$\.\|\?\+\(\)]/, "\\$&")
if output[0] == '*' if output[0] == '*'
prefix = '/'
output = output.replace(/^\*+/g, '') output = output.replace(/^\*+/g, '')
else else
prefix = '/^' output = '^' + output
if output[output.length - 1] == '*' if output[output.length - 1] == '*'
suffix = '/'
output = output.replace(/\*+$/g, '') output = output.replace(/\*+$/g, '')
else else
suffix = '$/' output = output + '$'
output = output.replace(/[\*]/, '.*') output = output.replace(/[\*]/, '.*')
return eval(prefix + output + suffix) return new RegExp(output, flags)
normalizers = {} normalizers = {}
@@ -66,9 +89,9 @@ normalizers.date = (input, normType) ->
normalizers.string = (input, normType) -> normalizers.string = (input, normType) ->
if normType is 'query' if normType is 'query'
if /^\/(.*?)\/(g?i?m?y?)$/.test(input) if (m = /^\/(.*?)\/(g?i?m?y?)$/.exec(input))
return [{'$regex' : eval(input)}, input] return [{'$regex' : new RegExp(m[1], m[2])}, input]
input return input
colonizeMac = (input) -> colonizeMac = (input) ->
@@ -94,14 +117,14 @@ normalizers.mac = (input, normType) ->
if input.indexOf('*') != -1 if input.indexOf('*') != -1
input = (colonizeMac(a) for a in input.split('*')).join('*') input = (colonizeMac(a) for a in input.split('*')).join('*')
return stringToRegexp(input, false) return stringToRegexp(input, 'i')
input = colonizeMac(input) input = colonizeMac(input)
if input.length == 17 if input.length == 17
return input return [input, input.toLowerCase()]
return {'$regex' : eval('/' + input + '/')} return {'$regex' : new RegExp(input, 'i')}
exports.normalize = (path, value, normType) -> exports.normalize = (path, value, normType) ->
+19 -2
View File
@@ -1,3 +1,20 @@
###
# Copyright 2013, 2014 Zaid Abdulla
#
# 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/>.
###
config = require './config' config = require './config'
db = require './db' db = require './db'
@@ -160,7 +177,7 @@ compileAliases = (callback) ->
counter = 0 counter = 0
splitIds(4, (batches) -> splitIds(4, (batches) ->
if not batches? if not batches?
return callback(null, aliases, config.ALIASES_CACHE) return callback(null, aliases)
for batch in batches for batch in batches
options = { options = {
@@ -184,7 +201,7 @@ compileAliases = (callback) ->
aliases[alias].push(id) aliases[alias].push(id)
if ++counter == batches.length if ++counter == batches.length
callback?(null, aliases, config.ALIASES_CACHE) callback?(null, aliases)
) )
return return
) )
+90 -40
View File
@@ -1,3 +1,43 @@
###
# Copyright 2013, 2014 Zaid Abdulla
#
# 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/>.
#
# This file incorporates work covered by the following copyright and
# permission notice:
#
# Copyright 2013 Fanoos Telecom
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
###
config = require './config' config = require './config'
common = require './common' common = require './common'
db = require './db' db = require './db'
@@ -14,12 +54,21 @@ exports.calculatePresetsHash = (presets, objects) ->
matchObject = (object, param) -> matchObject = (object, param) ->
return false if not object._keys? or object._keys.length == 0 flatObject = common.flattenObject(object)
for k in object._keys if object._keys?.length
return false if not param[k]? keys = object._keys
v = common.matchType(param[k]._value, object[k]) else
if param[k]._value != v # Consider all parameters as keys if none are defined
keys = (k for k in Object.keys(flatObject) when k[0] != '_')
for k in keys
p = common.getParamValueFromPath(param, k)
return false if not p?
v = common.matchType(p._value, flatObject[k])
if p._value != common.matchType(p._value, flatObject[k])
return false return false
return true return true
@@ -44,6 +93,7 @@ exports.getDevicePreset = (deviceId, presets, objects, aliases, callback) ->
if commandfile of deviceCustomCommands if commandfile of deviceCustomCommands
projection["_customCommands.#{c.command.split(' ', 1)}"] = 1 projection["_customCommands.#{c.command.split(' ', 1)}"] = 1
when 'software_version' when 'software_version'
projection['Device.DeviceInfo.SoftwareVersion'] = 1
projection['InternetGatewayDevice.DeviceInfo.SoftwareVersion'] = 1 projection['InternetGatewayDevice.DeviceInfo.SoftwareVersion'] = 1
when 'add_tag', 'delete_tag' when 'add_tag', 'delete_tag'
projection['_tags'] = 1 projection['_tags'] = 1
@@ -105,15 +155,7 @@ exports.getDevicePreset = (deviceId, presets, objects, aliases, callback) ->
devicePreset.objects[c.name][c.object].current ?= {} devicePreset.objects[c.name][c.object].current ?= {}
for k,p of param for k,p of param
continue if k[0] == '_' continue if k[0] == '_'
if p._name? if matchObject(objects[c.object], p)
if p._name == c.object
devicePreset.objects[c.name][c.object].current[k] = p
else if matchObject(objects[c.object], p)
u = {}
u["#{c.name}.#{k}._name"] = c.object
db.devicesCollection.update({'_id' : deviceId}, {'$set' : u}, {}, (err, count) ->
throw err if err
)
devicePreset.objects[c.name][c.object].current[k] = p devicePreset.objects[c.name][c.object].current[k] = p
when 'delete_object' when 'delete_object'
@@ -124,15 +166,7 @@ exports.getDevicePreset = (deviceId, presets, objects, aliases, callback) ->
devicePreset.objects[c.name][c.object].current ?= {} devicePreset.objects[c.name][c.object].current ?= {}
for k,p of param for k,p of param
continue if k[0] == '_' continue if k[0] == '_'
if p._name? if matchObject(objects[c.object], p)
if p._name == c.object
devicePreset.objects[c.name][c.object].current[k] = p
else if matchObject(objects[c.object], p)
u = {}
u["#{c.name}.#{k}._name"] = c.object
db.devicesCollection.update({'_id' : deviceId}, {'$set' : u}, {}, (err, count) ->
throw err if err
)
devicePreset.objects[c.name][c.object].current[k] = p devicePreset.objects[c.name][c.object].current[k] = p
when 'custom_command_value' when 'custom_command_value'
@@ -164,7 +198,10 @@ exports.getDevicePreset = (deviceId, presets, objects, aliases, callback) ->
when 'software_version' when 'software_version'
devicePreset.softwareVersion ?= {} devicePreset.softwareVersion ?= {}
devicePreset.softwareVersion.preset = c.software_version devicePreset.softwareVersion.preset = c.software_version
devicePreset.softwareVersion.current = common.getParamValueFromPath(device, 'InternetGatewayDevice.DeviceInfo.SoftwareVersion') if device?.Device? # TR-181 data model
devicePreset.softwareVersion.current = common.getParamValueFromPath(device, 'Device.DeviceInfo.SoftwareVersion')
else # TR-098 data model
devicePreset.softwareVersion.current = common.getParamValueFromPath(device, 'InternetGatewayDevice.DeviceInfo.SoftwareVersion')
else else
throw new Error('Unknown configuration type') throw new Error('Unknown configuration type')
@@ -174,8 +211,9 @@ exports.getDevicePreset = (deviceId, presets, objects, aliases, callback) ->
exports.processDevicePreset = (deviceId, devicePreset, callback) -> exports.processDevicePreset = (deviceId, devicePreset, callback) ->
PRESETS_TIME_PADDING = config.get('PRESETS_TIME_PADDING')
now = new Date() now = new Date()
expiry = config.PRESETS_CACHE_DURATION expiry = config.get('PRESETS_CACHE_DURATION', deviceId)
getParameterValues = [] getParameterValues = []
setParameterValues = [] setParameterValues = []
addTags = [] addTags = []
@@ -192,7 +230,7 @@ exports.processDevicePreset = (deviceId, devicePreset, callback) ->
setParameterValues.push([parameterPath, presetValue, parameterDetails.current._type]) setParameterValues.push([parameterPath, presetValue, parameterDetails.current._type])
else if parameterDetails.preset.expiry? else if parameterDetails.preset.expiry?
diff = parameterDetails.preset.expiry - (now - (parameterDetails.current._timestamp ? 0)) / 1000 diff = parameterDetails.preset.expiry - (now - (parameterDetails.current._timestamp ? 0)) / 1000
if diff <= config.PRESETS_TIME_PADDING if diff <= PRESETS_TIME_PADDING
if parameterDetails.current._value? if parameterDetails.current._value?
getParameterValues.push(parameterPath) getParameterValues.push(parameterPath)
else else
@@ -205,20 +243,21 @@ exports.processDevicePreset = (deviceId, devicePreset, callback) ->
for objectName, objectDetails of object for objectName, objectDetails of object
continue if not objectDetails.current? continue if not objectDetails.current?
if objectDetails.preset? if objectDetails.preset?
flatObject = common.flattenObject(objectDetails.preset)
if Object.keys(objectDetails.current).length > 0 if Object.keys(objectDetails.current).length > 0
for i, obj of objectDetails.current for i, obj of objectDetails.current
for paramName, paramValue of objectDetails.preset for paramName, paramValue of flatObject
continue if paramName[0] == '_' continue if paramName[0] == '_'
if obj[paramName] currentValue = common.getParamValueFromPath(obj, paramName)
currentValue = obj[paramName]._value if currentValue?
presetValue = common.matchType(currentValue, objectDetails.preset[paramName]) presetValue = common.matchType(currentValue._value, flatObject[paramName])
if currentValue != presetValue if currentValue._value != presetValue
setParameterValues.push(["#{parameterPath}.#{i}.#{paramName}", presetValue, obj[paramName]._type]) setParameterValues.push(["#{parameterPath}.#{i}.#{paramName}", presetValue, currentValue._type])
else else
vals = [] vals = []
for k,v of objectDetails.preset for k,v of flatObject
vals.push([k, v]) if k[0] != '_' vals.push([k, v]) if k[0] != '_'
taskList.push({device : deviceId, name : 'addObject', objectName : parameterPath, parameterValues : vals, instanceName : objectName}) taskList.push({device : deviceId, name : 'addObject', objectName : parameterPath, parameterValues : vals})
else if Object.keys(objectDetails.current).length > 0 else if Object.keys(objectDetails.current).length > 0
for i, obj of objectDetails.current for i, obj of objectDetails.current
taskList.push({device : deviceId, name : 'deleteObject', objectName : "#{parameterPath}.#{i}"}) taskList.push({device : deviceId, name : 'deleteObject', objectName : "#{parameterPath}.#{i}"})
@@ -240,7 +279,7 @@ exports.processDevicePreset = (deviceId, devicePreset, callback) ->
taskList.push({device : deviceId, name : 'customCommand', command : commandDetails.preset.valueCommand}) taskList.push({device : deviceId, name : 'customCommand', command : commandDetails.preset.valueCommand})
else if commandDetails.preset.expiry? else if commandDetails.preset.expiry?
diff = commandDetails.preset.expiry - (now - (commandDetails.current._timestamp ? 0)) / 1000 diff = commandDetails.preset.expiry - (now - (commandDetails.current._timestamp ? 0)) / 1000
if diff <= config.PRESETS_TIME_PADDING if diff <= PRESETS_TIME_PADDING
taskList.push({device : deviceId, name : 'customCommand', command : commandDetails.preset.expiryCommand}) taskList.push({device : deviceId, name : 'customCommand', command : commandDetails.preset.expiryCommand})
else else
expiry = Math.min(expiry, diff) expiry = Math.min(expiry, diff)
@@ -255,11 +294,19 @@ exports.processDevicePreset = (deviceId, devicePreset, callback) ->
getObjectHash = (object) -> getObjectHash = (object) ->
return object._id if not object._keys? or object._keys.length == 0 if object._keys?.length
hash = '' keys = object._keys
for k in object._keys else
hash += "#{k}=#{object[k]}" keys = (k for k in Object.keys(object) when k[0] != '_')
return hash
keys.sort()
hash = []
for k in keys
hash.push(k)
hash.push(common.getParamValueFromPath(object, k))
return JSON.stringify(hash)
accumulateConfigurations = (presets, objects) -> accumulateConfigurations = (presets, objects) ->
@@ -275,6 +322,9 @@ accumulateConfigurations = (presets, objects) ->
"object_#{c.name}_#{objectHash}" "object_#{c.name}_#{objectHash}"
when 'software_version' when 'software_version'
'software_version' 'software_version'
when 'custom_command_age', 'custom_command_value'
[filename, commandName] = c.command.split(' ', 2)
"#{c.type}_#{filename}_#{commandName}"
else else
"#{c.type}_#{c.name}" "#{c.type}_#{c.name}"
+48
View File
@@ -1,5 +1,28 @@
###
# Copyright 2013 Fanoos Telecom
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
###
common = require './common' common = require './common'
normalize = require './normalize' normalize = require './normalize'
mongodb = require 'mongodb'
expandValue = (param, value) -> expandValue = (param, value) ->
@@ -81,5 +104,30 @@ expand = (query, aliases) ->
return new_query return new_query
# Replace _id string values with ObjectID type
substituteObjectId = (query) ->
for k, v of query
if k[0] == '$' # logical operator
for i in [0...v.length]
substituteObjectId(v[i])
else if k == '_id'
if common.typeOf(v) is common.STRING_TYPE
query[k] = mongodb.ObjectID(v) if v.length == 24
else if common.typeOf(v) is common.OBJECT_TYPE
for kk, vv of v
switch kk # operator
when '$in', '$nin'
for i in [0...vv.length]
vv[i] = mongodb.ObjectID(vv[i]) if vv[i].length == 24
when '$eq', '$gt', '$gte', '$lt', '$lte', '$ne'
v[kk] = mongodb.ObjectID(vv) if vv.length == 24
when '$exists', '$type' then
# ignore
else
throw new Error('Operator not supported')
return query
exports.expand = expand exports.expand = expand
exports.substituteObjectId = substituteObjectId
+83
View File
@@ -0,0 +1,83 @@
###
# Copyright 2013, 2014 Zaid Abdulla
#
# 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/>.
###
domain = require 'domain'
util = require 'util'
cluster = require 'cluster'
config = require './config'
db = require './db'
service = config.argv[0]
if not service?
throw new Error('Missing argument cwmp, fs, or nbi')
networkInterface = config.get("#{service.toUpperCase()}_INTERFACE")
port = config.get("#{service.toUpperCase()}_PORT")
useHttps = config.get("#{service.toUpperCase()}_SSL")
serviceListener = require("./#{service}").listener
server = null
listener = (httpRequest, httpResponse) ->
d = domain.create()
d.on('error', (err) ->
util.error("#{new Date().toISOString()} - #{err.stack}\n")
try
killTimer = setTimeout(() ->
process.exit(1)
, 30000)
killTimer.unref()
cluster.worker?.disconnect()
server.close(() ->
db.disconnect()
)
httpResponse.writeHead(500, {'Connection' : 'close'})
httpResponse.end("#{err.name}: #{err.message}")
catch err2
util.error("#{new Date().toISOString()} - #{err2.stack}\n")
)
d.add(httpRequest)
d.add(httpResponse)
d.run(() ->
serviceListener(httpRequest, httpResponse)
)
if useHttps
path = require 'path'
fs = require 'fs'
httpsKey = path.resolve(config.get('CONFIG_DIR'), "#{service}.key")
httpsCert = path.resolve(config.get('CONFIG_DIR'), "#{service}.crt")
options = {
key: fs.readFileSync(httpsKey),
cert: fs.readFileSync(httpsCert)
}
server = require('https').createServer(options, listener)
else
server = require('http').createServer(listener)
db.connect((err) ->
throw err if err
server.listen(port, networkInterface)
)
+56 -40
View File
@@ -1,3 +1,43 @@
###
# Copyright 2013, 2014 Zaid Abdulla
#
# 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/>.
#
# This file incorporates work covered by the following copyright and
# permission notice:
#
# Copyright 2013 Fanoos Telecom
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
###
config = require './config' config = require './config'
libxmljs = require 'libxmljs' libxmljs = require 'libxmljs'
@@ -41,9 +81,15 @@ NAMESPACES = {
} }
} }
# Generate Libxmljs options
LIBXMLJS_OPTIONS = {}
for k, v of config.allConfig
if k.lastIndexOf('XML_PARSE_', 0) == 0
LIBXMLJS_OPTIONS[k] = v
# Workaround for devices that don't use correct namespace declarations or prefixes # Workaround for devices that don't use correct namespace declarations or prefixes
if config.IGNORE_XML_NAMESPACES if config.get('IGNORE_XML_NAMESPACES')
libxmljs.Element.prototype.__find = libxmljs.Element.prototype.find libxmljs.Element.prototype.__find = libxmljs.Element.prototype.find
libxmljs.Element.prototype.find = (xpath, namespaces) -> libxmljs.Element.prototype.find = (xpath, namespaces) ->
# Modify xpath queries to work regardless of element's namespace # Modify xpath queries to work regardless of element's namespace
@@ -52,25 +98,6 @@ if config.IGNORE_XML_NAMESPACES
libxmljs.Element.prototype.__find.call(this, p, namespaces) libxmljs.Element.prototype.__find.call(this, p, namespaces)
# 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
cookieEncode = (value) ->
new Buffer(value).toString('base64').replace(/\/|\+|\=/g, (c) -> if c is '/' then '_' else if c is '+' then '-' else '')
cookieDecode = (encodedValue) ->
new Buffer(encodedValue, 'base64').toString()
cookiesToObj = (cookieLine) ->
cookies = {}
while match = COOKIE_REGEX.exec(cookieLine)
cookies[cookieDecode(match[1])] = cookieDecode(match[2])
return cookies
cookiesToStr = (obj) ->
"#{cookieEncode(cn)}=#{cookieEncode(cv)}" for cn, cv of obj
event = (xml) -> event = (xml) ->
e.text().trim() for e in xml.find('EventStruct/EventCode') e.text().trim() for e in xml.find('EventStruct/EventCode')
@@ -205,7 +232,7 @@ acsInform = (xml) ->
parameterList : parameterValueList(xml.get('ParameterList')), parameterList : parameterValueList(xml.get('ParameterList')),
deviceId : traverseXml(xml.get('DeviceId')), deviceId : traverseXml(xml.get('DeviceId')),
event : event(xml.get('Event')), event : event(xml.get('Event')),
retryCount : JSON.parse(xml.get('RetryCount').text()) retryCount : parseInt(xml.get('RetryCount').text())
} }
@@ -264,22 +291,16 @@ fault = (xml) ->
return traverseXml(xml) return traverseXml(xml)
exports.request = (httpRequest) -> exports.request = (httpRequest, cwmpVersion) ->
cwmpRequest = {cookies: {}} cwmpRequest = {cwmpVersion : cwmpVersion}
cwmpRequest.cookies = cookiesToObj(httpRequest.headers.cookie) if httpRequest.headers.cookie
cwmpRequest.cwmpVersion = cwmpRequest.cookies.cwmpVersion
data = httpRequest.getBody() data = httpRequest.getBody()
if data.length > 0 if data.length > 0
try xml = libxmljs.parseXml(data, LIBXMLJS_OPTIONS)
xml = libxmljs.parseXml data
catch err
# some devices send invalid utf8 characters
xml = libxmljs.parseXml httpRequest.getBody('binary')
if not cwmpRequest.cwmpVersion? if not cwmpRequest.cwmpVersion?
# cwmpVersion doesn't exist in cookies, thus it's an inform request # cwmpVersion not passed, thus it's an inform request
methodElement = xml.get('/soap-env:Envelope/soap-env:Body/*', NAMESPACES['1.0']) methodElement = xml.get('/soap-env:Envelope/soap-env:Body/*', NAMESPACES['1.0'])
switch methodElement.namespace().href() switch methodElement.namespace().href()
when 'urn:dslforum-org:cwmp-1-0' when 'urn:dslforum-org:cwmp-1-0'
@@ -303,7 +324,7 @@ exports.request = (httpRequest) ->
cwmpRequest.id = try xml.get('/soap-env:Envelope/soap-env:Header/cwmp:ID', NAMESPACES[cwmpRequest.cwmpVersion]).text() catch then null cwmpRequest.id = try xml.get('/soap-env:Envelope/soap-env:Header/cwmp:ID', NAMESPACES[cwmpRequest.cwmpVersion]).text() catch then null
if methodElement? if methodElement? and not (config.get('IGNORE_XML_NAMESPACES') and methodElement.name() is 'Fault')
switch methodElement.name() switch methodElement.name()
when 'Inform' when 'Inform'
cwmpRequest.methodRequest = acsInform(methodElement) cwmpRequest.methodRequest = acsInform(methodElement)
@@ -360,7 +381,6 @@ createSoapEnv = (cwmpVersion) ->
exports.response = (cwmpResponse) -> exports.response = (cwmpResponse) ->
headers = { headers = {
'Content-Type' : 'text/xml; charset="utf-8"',
'Server' : SERVER_NAME, 'Server' : SERVER_NAME,
'SOAPServer' : SERVER_NAME, 'SOAPServer' : SERVER_NAME,
} }
@@ -372,8 +392,6 @@ exports.response = (cwmpResponse) ->
body = env.node('soap-env:Body') body = env.node('soap-env:Body')
switch cwmpResponse.methodResponse.type switch cwmpResponse.methodResponse.type
when 'InformResponse' when 'InformResponse'
cwmpResponse.cookies ?= {}
cwmpResponse.cookies.cwmpVersion = cwmpResponse.cwmpVersion
acsInformResponse(body, cwmpResponse.methodResponse) acsInformResponse(body, cwmpResponse.methodResponse)
when 'GetRPCMethodsResponse' when 'GetRPCMethodsResponse'
acsGetRPCMethodsResponse(body, cwmpResponse.methodResponse) acsGetRPCMethodsResponse(body, cwmpResponse.methodResponse)
@@ -408,10 +426,8 @@ exports.response = (cwmpResponse) ->
else else
throw Error("Unknown method request #{cwmpResponse.methodRequest.type}") throw Error("Unknown method request #{cwmpResponse.methodRequest.type}")
if cwmpResponse?.cookies? and Object.keys(cwmpResponse.cookies).length > 0
headers['Set-Cookie'] = cookiesToStr(cwmpResponse.cookies)
if env? if env?
return {code: 200, headers: headers, data: env.doc().toString()} headers['Content-Type'] = 'text/xml; charset="utf-8"'
return {code: 200, headers: headers, data: new Buffer(env.doc().toString(false))}
else else
return {code: 204, headers: headers, data: ''} return {code: 204, headers: headers, data: new Buffer(0)}
+97 -79
View File
@@ -1,9 +1,49 @@
###
# Copyright 2013, 2014 Zaid Abdulla
#
# 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/>.
#
# This file incorporates work covered by the following copyright and
# permission notice:
#
# Copyright 2013 Fanoos Telecom
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
###
config = require './config' config = require './config'
common = require './common' common = require './common'
db = require './db' db = require './db'
mongodb = require 'mongodb' mongodb = require 'mongodb'
customCommands = require './custom-commands' customCommands = require './custom-commands'
BATCH_SIZE = 16 url = require 'url'
exports.STATUS_OK = STATUS_OK = 1 exports.STATUS_OK = STATUS_OK = 1
exports.STATUS_FAULT = STATUS_FAULT = 2 exports.STATUS_FAULT = STATUS_FAULT = 2
@@ -17,7 +57,7 @@ this.refreshObject = (task, methodResponse, callback) ->
if not task.session.subtask? if not task.session.subtask?
path = task.objectName path = task.objectName
path += '.' if path != '' and not common.endsWith(path, '.') path += '.' if path != '' and not common.endsWith(path, '.')
task.session.subtask = {device : task.device, name : 'getParameterNames', parameterPath : path, nextLevel : false} task.session.subtask = {device : task.device, name : 'getParameterNames', parameterPath : path}
if methodResponse.faultcode? if methodResponse.faultcode?
task.fault = methodResponse task.fault = methodResponse
@@ -26,12 +66,16 @@ this.refreshObject = (task, methodResponse, callback) ->
if task.session.subtask.name is 'getParameterNames' if task.session.subtask.name is 'getParameterNames'
this.getParameterNames(task.session.subtask, methodResponse, (err, status, methodRequest, deviceUpdates) => this.getParameterNames(task.session.subtask, methodResponse, (err, status, methodRequest, deviceUpdates) =>
if status & STATUS_COMPLETED task.session.parameterNames ?= []
parameterNames = [] if deviceUpdates?.parameterNames?
for p in deviceUpdates.parameterNames for p in deviceUpdates.parameterNames
if not common.endsWith(p[0], '.') if not common.endsWith(p[0], '.')
parameterNames.push(p[0]) task.session.parameterNames.push(p[0])
task.session.subtask = {name : 'getParameterValues', parameterNames : parameterNames}
if status & STATUS_COMPLETED
task.session.subtask = {device : task.device, name : 'getParameterValues', parameterNames : task.session.parameterNames}
delete task.session.parameterNames
this.getParameterValues(task.session.subtask, {}, (err, status, methodRequest) -> this.getParameterValues(task.session.subtask, {}, (err, status, methodRequest) ->
# ignore deviceUpdates returned by firt call to getParameterValues # ignore deviceUpdates returned by firt call to getParameterValues
callback(err, status, methodRequest, deviceUpdates) callback(err, status, methodRequest, deviceUpdates)
@@ -39,7 +83,7 @@ this.refreshObject = (task, methodResponse, callback) ->
else if status & STATUS_OK else if status & STATUS_OK
callback(err, STATUS_OK, methodRequest, deviceUpdates) callback(err, STATUS_OK, methodRequest, deviceUpdates)
else else
throw Error('Unexpected subtask status') throw new Error('Unexpected subtask status')
) )
else if task.session.subtask.name is 'getParameterValues' else if task.session.subtask.name is 'getParameterValues'
if methodResponse.faultcode? if methodResponse.faultcode?
@@ -53,88 +97,55 @@ this.refreshObject = (task, methodResponse, callback) ->
throw Error('Unexpected subtask name') throw Error('Unexpected subtask name')
findMissingParameters = (device, parameterList, prefix) ->
missingParameters = []
paths = {}
for param in parameterList
p = param[0]
paths[p] = true
i = p.indexOf('.')
while i != -1
paths[p[0...i]] = true
i = p.indexOf('.', i + 1)
recursive = (obj, prefix) ->
for k,v of obj
continue if k[0] == '_'
p = prefix + k
if paths[p]
recursive(v, "#{p}.")
else
missingParameters.push(p)
recursive(device, prefix)
return missingParameters
this.getParameterNames = (task, methodResponse, callback) -> this.getParameterNames = (task, methodResponse, callback) ->
if methodResponse.faultcode? if methodResponse.faultcode?
task.fault = methodResponse task.fault = methodResponse
callback(null, STATUS_FAULT) callback(null, STATUS_FAULT)
return return
GET_PARAMETER_NAMES_DEPTH_THRESHOLD = config.get('GET_PARAMETER_NAMES_DEPTH_THRESHOLD', task.device)
getParameterDepth = (param) ->
return 0 if !param
return (param[...-1] + '.').split('.').length - 1
if methodResponse.type is 'GetParameterNamesResponse' if methodResponse.type is 'GetParameterNamesResponse'
# Make sure that for each parameters, all its parents are included path = task.session.queue.pop()
# If parameter depth higher than the threshold, nextLevel was set to false
if not task.nextLevel? and getParameterDepth(path) < GET_PARAMETER_NAMES_DEPTH_THRESHOLD
for p in methodResponse.parameterList
task.session.queue.push(p[0]) if p[0][-1..] == '.'
deviceUpdates = {parameterNames : methodResponse.parameterList}
# Make sure that for each parameter, all its parents are explicitly included
found = {} found = {}
for p in methodResponse.parameterList found[path] = 0 if !!path
for p in deviceUpdates.parameterNames
param = p[0] param = p[0]
i = param.length i = param.length
while (i = param.lastIndexOf('.', i-1)) > task.parameterPath.length while (i = param.lastIndexOf('.', i-1)) > path.length
pp = param.slice(0, i + 1) pp = param.slice(0, i + 1)
break if found[pp]? break if found[pp]?
found[pp] = 0 found[pp] = 0
found[p[0]] = 1 found[p[0]] = 1
for k, v of found for k, v of found
methodResponse.parameterList.push([k]) if v == 0 deviceUpdates.parameterNames.push([k]) if v == 0
deviceUpdates = {parameterNames : methodResponse.parameterList}
path = if common.endsWith(task.parameterPath, '.') then task.parameterPath.slice(0, -1) else task.parameterPath
projection = {}
projection[path] = 1 if !!task.parameterPath
# delete nonexisting params
db.devicesCollection.findOne({_id : task.device}, projection, (err, device) ->
if device
root = device
rootPath = ''
if !!task.parameterPath
ps = path.split('.')
for p in ps
rootPath += "#{p}."
root = root[p]
break if not root?
if root
deviceUpdates.deletedObjects = findMissingParameters(root, methodResponse.parameterList, rootPath)
else
# avoid adding and deleting the same param
if rootPath isnt task.parameterPath
deviceUpdates.deletedObjects = [rootPath.slice(0, -1)]
# some devices don't return the root object as described in the standard. add manually to update timestamp
deviceUpdates.parameterNames.push([task.parameterPath]) if !!task.parameterPath
callback(null, STATUS_COMPLETED, null, deviceUpdates)
)
else else
task.session = {queue : [task.parameterPath]}
if task.session.queue.length > 0
path = task.session.queue[-1..][0]
methodRequest = { methodRequest = {
type : 'GetParameterNames', type : 'GetParameterNames',
parameterPath : task.parameterPath, parameterPath : path,
nextLevel : if task.nextLevel? then task.nextLevel else false nextLevel : task.nextLevel ? getParameterDepth(path) < GET_PARAMETER_NAMES_DEPTH_THRESHOLD
} }
callback(null, STATUS_OK, methodRequest) return callback(null, STATUS_OK, methodRequest, deviceUpdates)
else
return callback(null, STATUS_COMPLETED, null, deviceUpdates)
this.getParameterValues = (task, methodResponse, callback) -> this.getParameterValues = (task, methodResponse, callback) ->
@@ -150,7 +161,7 @@ this.getParameterValues = (task, methodResponse, callback) ->
else if methodResponse.parameterList? else if methodResponse.parameterList?
task.session.currentIndex = task.session.nextIndex task.session.currentIndex = task.session.nextIndex
task.session.nextIndex = Math.min(task.session.currentIndex + BATCH_SIZE, task.parameterNames.length) task.session.nextIndex = Math.min(task.session.currentIndex + config.get('TASK_PARAMETERS_BATCH_SIZE', task.device), task.parameterNames.length)
names = task.parameterNames.slice(task.session.currentIndex, task.session.nextIndex) names = task.parameterNames.slice(task.session.currentIndex, task.session.nextIndex)
if methodResponse.type is 'GetParameterValuesResponse' if methodResponse.type is 'GetParameterValuesResponse'
@@ -180,7 +191,7 @@ this.setParameterValues = (task, methodResponse, callback) ->
prevValues = task.parameterValues.slice(task.session.currentIndex, task.session.nextIndex) prevValues = task.parameterValues.slice(task.session.currentIndex, task.session.nextIndex)
task.session.currentIndex = task.session.nextIndex task.session.currentIndex = task.session.nextIndex
task.session.nextIndex = Math.min(task.session.currentIndex + BATCH_SIZE, task.parameterValues.length) task.session.nextIndex = Math.min(task.session.currentIndex + config.get('TASK_PARAMETERS_BATCH_SIZE', task.device), task.parameterValues.length)
values = task.parameterValues.slice(task.session.currentIndex, task.session.nextIndex) values = task.parameterValues.slice(task.session.currentIndex, task.session.nextIndex)
if prevValues? if prevValues?
@@ -206,10 +217,10 @@ this.addObject = (task, methodResponse, callback) ->
if methodResponse.type is 'AddObjectResponse' if methodResponse.type is 'AddObjectResponse'
status_save = STATUS_SAVE status_save = STATUS_SAVE
task.session.instanceNumber = methodResponse.instanceNumber task.session.instanceNumber = methodResponse.instanceNumber
# TODO Don't specify nextLevel explicity. Also consider using refreshObject instead.
task.session.subtask = {device : task.device, name : 'getParameterNames', parameterPath : "#{task.objectName}.#{task.session.instanceNumber}.", nextLevel : false} task.session.subtask = {device : task.device, name : 'getParameterNames', parameterPath : "#{task.objectName}.#{task.session.instanceNumber}.", nextLevel : false}
task.session.appliedParameterValues = [] task.session.appliedParameterValues = []
task.session.parameterNames = [] task.session.parameterNames = []
allDeviceUpdates.instanceName = [["#{task.objectName}.#{task.session.instanceNumber}", task.instanceName]] if task.instanceName?
else else
callback(null, STATUS_OK, {type : 'AddObject', objectName : "#{task.objectName}."}) callback(null, STATUS_OK, {type : 'AddObject', objectName : "#{task.objectName}."})
return return
@@ -224,7 +235,7 @@ this.addObject = (task, methodResponse, callback) ->
task.session.parameterNames.push(p[0]) if not common.endsWith(p[0], '.') task.session.parameterNames.push(p[0]) if not common.endsWith(p[0], '.')
if status & STATUS_COMPLETED if status & STATUS_COMPLETED
task.session.subtask = {name : 'getParameterValues', parameterNames : task.session.parameterNames} task.session.subtask = {device : task.device, name : 'getParameterValues', parameterNames : task.session.parameterNames}
subtask() subtask()
else if status & STATUS_OK else if status & STATUS_OK
# Use STATUS_SAVE to avoid adding duplicate object in case of error # Use STATUS_SAVE to avoid adding duplicate object in case of error
@@ -240,7 +251,7 @@ this.addObject = (task, methodResponse, callback) ->
if deviceUpdates and deviceUpdates.parameterValues? if deviceUpdates and deviceUpdates.parameterValues?
for p1 in deviceUpdates.parameterValues for p1 in deviceUpdates.parameterValues
for p2 in task.parameterValues for p2 in task.parameterValues
if common.endsWith(p1[0], ".#{p2[0]}") if p1[0] == "#{task.objectName}.#{task.session.instanceNumber}.#{p2[0]}"
t = if p2[2] then p2[2] else p1[2] t = if p2[2] then p2[2] else p1[2]
v = common.matchType(p1[1], p2[1]) v = common.matchType(p1[1], p2[1])
# TODO only include if writable # TODO only include if writable
@@ -251,7 +262,7 @@ this.addObject = (task, methodResponse, callback) ->
return callback(null, STATUS_FAULT) return callback(null, STATUS_FAULT)
if status & STATUS_COMPLETED and task.session.appliedParameterValues.length > 0 if status & STATUS_COMPLETED and task.session.appliedParameterValues.length > 0
task.session.subtask = {name : 'setParameterValues', parameterValues : task.session.appliedParameterValues} task.session.subtask = {device : task.device, name : 'setParameterValues', parameterValues : task.session.appliedParameterValues}
subtask() subtask()
else else
callback(err, status, methodRequest, allDeviceUpdates) callback(err, status, methodRequest, allDeviceUpdates)
@@ -285,7 +296,7 @@ this.reboot = (task, methodResponse, callback) ->
task.fault = methodResponse task.fault = methodResponse
callback(null, STATUS_FAULT) callback(null, STATUS_FAULT)
return return
if methodResponse.type isnt 'RebootResponse' if methodResponse.type isnt 'RebootResponse'
callback(null, STATUS_OK, {type : 'Reboot'}) callback(null, STATUS_OK, {type : 'Reboot'})
else else
@@ -319,11 +330,18 @@ this.download = (task, methodResponse, callback) ->
callback(err) callback(err)
return return
l = {
protocol : if config.get('FS_SSL') then 'https' else 'http',
hostname : config.get('FS_IP'),
port : config.get('FS_PORT'),
pathname : encodeURIComponent(file.filename)
}
methodRequest = { methodRequest = {
type : 'Download', type : 'Download',
fileType : file.metadata.fileType, fileType : file.metadata.fileType,
fileSize : file.length, fileSize : file.length,
url : "http://#{config.FILES_IP}:#{config.FILES_PORT}/#{encodeURIComponent(file.filename)}", url : url.format(l),
successUrl : task.successUrl, successUrl : task.successUrl,
failureUrl : task.failureUrl failureUrl : task.failureUrl
} }
@@ -336,8 +354,8 @@ this.download = (task, methodResponse, callback) ->
this.customCommand = (task, methodResponse, callback) -> this.customCommand = (task, methodResponse, callback) ->
# TODO implement timeout # TODO implement timeout
customCommands.execute(task.device, task.command, (err, value) -> customCommands.execute(task.device, task.command, (err, value) ->
if err? if err
task.fault = err task.fault = {name : err.name, message : err.message}
callback(null, STATUS_FAULT) callback(null, STATUS_FAULT)
else else
commandName = task.command.split(' ', 2)[0] commandName = task.command.split(' ', 2)[0]
+217
View File
@@ -0,0 +1,217 @@
###
# Copyright 2013, 2014 Zaid Abdulla
#
# 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/>.
###
common = require './common'
db = require './db'
normalize = require('./normalize').normalize
mongoQuery = require './mongo-query'
parameters = require './parameters'
OBJECT_REGEX = /\.$/
INSTANCE_REGEX = /\.[\d]+\.$/
queueUpdates = (deviceId, deviceUpdates, callback) ->
return callback() if not deviceUpdates? or Object.keys(deviceUpdates).length == 0
# TODO consider setting expiry
db.redisClient.rpush("#{deviceId}_updates", JSON.stringify(deviceUpdates), (err, res) ->
return callback(err)
)
clearUpdatesQueue = (deviceId, callback) ->
db.redisClient.del("#{deviceId}_updates", (err, res) ->
return callback(err)
)
commitUpdates = (deviceId, deviceUpdates, commitQueue, callback) ->
now = new Date(Date.now())
update = {'$set' : {}, '$unset' : {}}
# Used to find parameters that no longer exist in device
presentPaths = null
projection = null
# Keep track of alias changes to clear cache if needed
newAliases = null
processBatch = (updatesBatch, cb) ->
if updatesBatch.informEvents?
update['$set']['_lastInform'] = now
update['$set']['_lastBoot'] if '1 BOOT' in updatesBatch.informEvents
update['$set']['_lastBootstrap'] if '0 BOOTSTRAP' in updatesBatch.informEvents
if updatesBatch.parameterValues?
for p in updatesBatch.parameterValues
origValue = p[1]
v = normalize(p[0], origValue)
path = if common.endsWith(p[0], '.') then p[0] else "#{p[0]}."
if v == origValue
update['$unset']["#{path}_orig"] = 1
else
update['$set']["#{path}_orig"] = origValue
update['$set']["#{path}_value"] = v
update['$set']["#{path}_timestamp"] = now
if p[2]?
update['$set']["#{path}_type"] = p[2]
if updatesBatch.deletedObjects?
for p in updatesBatch.deletedObjects
update['$unset'][p] = 1
if updatesBatch.parameterNames?
presentPaths ?= {}
projection ?= {}
for p in updatesBatch.parameterNames
if OBJECT_REGEX.test(p[0])
path = p[0].slice(0, -1)
if INSTANCE_REGEX.test(p[0])
update['$set']["#{path}._instance"] = true
else
update['$set']["#{path}._object"] = true
else
path = p[0]
update['$set']["#{path}._writable"] = p[1] if p[1]?
update['$set']["#{path}._timestamp"] = now
# Add parameter (sans any dot suffix) if has an alias
if (a = parameters.getAlias(path))?
newAliases ?= {}
newAliases[path] = a
projection[path] = 1
# Store this param path and all its parents for later look up
presentPaths[path] = true
i = p[0].indexOf('.')
while i != -1
presentPaths[p[0][0...i]] = true
i = p[0].indexOf('.', i + 1)
if updatesBatch.customCommands?
for p in updatesBatch.customCommands
commandName = p[0]
update['$set']["_customCommands.#{commandName}._value"] = p[1]
update['$set']["_customCommands.#{commandName}._timestamp"] = now
processBatches = (cb) ->
if not commitQueue
processBatch(deviceUpdates) if deviceUpdates?
return cb()
db.redisClient.lpop("#{deviceId}_updates", (err, res) ->
callback(err) if err
if not res?
processBatch(deviceUpdates) if deviceUpdates?
return cb()
processBatch(JSON.parse(res))
return processBatches(cb)
)
commit = () ->
for k of update
delete update[k] if Object.keys(update[k]).length == 0
if Object.keys(update).length
db.devicesCollection.update({'_id' : deviceId}, update, {}, (err, count) ->
return callback(err) if err
# Clear aliases cache if there's a new aliased parameter that has not been included in aliases cache
if newAliases?
newAliasFound = false
db.getAliases((aliases) ->
for p, a of newAliases
if not aliases[a]? or p not in aliases[a]
newAliasFound = true
break
return callback() if not newAliasFound
db.redisClient.del('aliases', (err, res) ->
return callback(err)
)
)
else
return callback()
)
else
return callback()
processBatches(() ->
if projection?
mongoQuery.optimizeProjection(projection)
# Fetch device from DB to discover and delete parameters that no longer exist
# While we're fetching the object, we might as well use the information
# to optimize our update query
db.devicesCollection.findOne({'_id' : deviceId}, projection, (err, device) ->
return callback(err) if err
recursive = (obj, prefix) ->
for k, v of obj
p = prefix + k
# Mark unset operations we want to keep
if update['$unset'][p]?
update['$unset'][p] = 2
continue
if k[0] == '_'
# Remove set operations for unmodified values for optimization
if update['$set'][p] == v
delete update['$set'][p]
continue
if presentPaths[p]
recursive(v, "#{p}.")
else
update['$unset'][p] = 2
# Scan the DB object and remove parameters that no longer exists in device
for path of projection
root = device
rootPath = ''
for p in path.split('.')
rootPath += "#{p}."
root = root[p]
break if not root?
if root
recursive(root, rootPath)
else
if not presentPaths[rootPath.slice(0, -1)]?
update['$unset'][rootPath.slice(0, -1)] = 2
# Remove unnecessary unset updates
for k, v of update['$unset']
if common.startsWith(k, path + '.') and v != 2
delete update['$unset'][k]
commit()
)
else
commit()
)
exports.queueUpdates = queueUpdates
exports.clearUpdatesQueue = clearUpdatesQueue
exports.commitUpdates = commitUpdates
+8 -8
View File
@@ -1,6 +1,6 @@
{ {
"name": "genieacs", "name": "genieacs",
"version": "0.9.9", "version": "1.0.5",
"description": "A blazing fast TR-069 auto configuration server (ACS) built with Node.js, Redis, and MongoDB", "description": "A blazing fast TR-069 auto configuration server (ACS) built with Node.js, Redis, and MongoDB",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -12,7 +12,7 @@
"ACS" "ACS"
], ],
"author": "Zaid Abdulla", "author": "Zaid Abdulla",
"license": "AGPL3", "license": "AGPL-3.0",
"readmeFilename": "README.md", "readmeFilename": "README.md",
"preferGlobal": true, "preferGlobal": true,
"directories": { "directories": {
@@ -20,13 +20,13 @@
"bin": "bin" "bin": "bin"
}, },
"dependencies": { "dependencies": {
"libxmljs": "~0.9.0", "libxmljs": "0.11.0 - 0.14.x",
"redis": "~0.10.1", "redis": "^0.12.1",
"hiredis": "~0.1.16", "hiredis": "0.1.17 - 0.4.x",
"mongodb": "~1.4.4" "mongodb": "^1.4.8"
}, },
"devDependencies": { "devDependencies": {
"coffee-script": "~1.7.1" "coffee-script": "^1.7.1"
}, },
"engines": { "engines": {
"node": ">=0.10.0 <0.12" "node": ">=0.10.0 <0.12"
@@ -34,7 +34,7 @@
"scripts": { "scripts": {
"prepublish": "npm run compile", "prepublish": "npm run compile",
"compile": "coffee -cb lib/*.coffee test/*.coffee tools/*.coffee", "compile": "coffee -cb lib/*.coffee test/*.coffee tools/*.coffee",
"configure": "cp ./config/config-sample.json ./config/config.json; cp ./config/parameters-sample.json ./config/parameters.json; cp ./config/custom_commands-sample.json ./config/custom_commands.json; cp ./config/auth-sample.js ./config/auth.js; cp ./config/httpscert-sample.key ./config/httpscert.key; cp ./config/httpscert-sample.crt ./config/httpscert.crt", "configure": "cp ./config/config-sample.json ./config/config.json; cp ./config/parameters-sample.json ./config/parameters.json; cp ./config/custom_commands-sample.json ./config/custom_commands.json; cp ./config/auth-sample.js ./config/auth.js",
"install": "npm run configure", "install": "npm run configure",
"test": "node test" "test": "node test"
} }
+17
View File
@@ -1,3 +1,20 @@
###
# Copyright 2013, 2014 Zaid Abdulla
#
# 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/>.
###
common = require '../lib/common' common = require '../lib/common'
NAMESPACES = { NAMESPACES = {
+17
View File
@@ -1,3 +1,20 @@
###
# Copyright 2013, 2014 Zaid Abdulla
#
# 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/>.
###
http = require 'http' http = require 'http'
libxmljs = require 'libxmljs' libxmljs = require 'libxmljs'
deviceTemplate = require './huawei_bm632w' deviceTemplate = require './huawei_bm632w'