Compare commits

...
45 Commits
Author SHA1 Message Date
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
Zaid Abdulla 2f65ecd3f9 0.9.9 2014-05-17 10:28:31 +03:00
Zaid Abdulla 70ffd8b653 Support for MongoDB 2.6. Preset precondition is now stored as a JSON string. 2014-05-17 09:35:05 +03:00
Zaid Abdulla 5bd7c99eea Allow space characters in quoted values in HTTP authentication header. Fixes #9. 2014-05-16 11:41:28 +03:00
31 changed files with 1460 additions and 533 deletions

No files matched your search

+2 -2
View File
@@ -54,11 +54,11 @@ You may now proceed with installing [GenieACS GUI front end](https://github.com/
## Support
Documentation is still work in progress. Feel free to contact me if you require any assistance in installation, configuration, or using the APIs.
Documentation is work in progress. Contact <support@genieacs.com> for any inquiry about installation, configuration, or using the API.
You may submit bug reports or feature requests [here](https://github.com/zaidka/genieacs/issues).
For commercial support options, please contact me.
For commercial support options, please visit [genieacs.com](http://genieacs.com).
## Contributing
+3 -4
View File
@@ -2,9 +2,8 @@
var path = require('path');
var fs = require('fs');
var dir = path.join(path.dirname(fs.realpathSync(__filename)), '../lib/');
process.title = 'genieacs-cwmp';
var dir = path.resolve(path.dirname(fs.realpathSync(__filename)), '..');
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 fs = require('fs');
var dir = path.join(path.dirname(fs.realpathSync(__filename)), '../lib/');
process.title = 'genieacs-fs';
var dir = path.resolve(path.dirname(fs.realpathSync(__filename)), '..');
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 fs = require('fs');
var dir = path.join(path.dirname(fs.realpathSync(__filename)), '../lib/');
process.title = 'genieacs-nbi';
var dir = path.resolve(path.dirname(fs.realpathSync(__filename)), '..');
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_SOCKET" : "/tmp/mongodb-27017.sock",
"REDIS_SOCKET" : "6379",
"ACS_INTERFACE" : "0.0.0.0",
"ACS_PORT" : 7547,
"ACS_HTTPS_INTERFACE" : "0.0.0.0",
"ACS_HTTPS_PORT" : 7548,
"API_INTERFACE" : "0.0.0.0",
"API_PORT" : 7557,
"FILES_INTERFACE" : "0.0.0.0",
"FILES_PORT" : 7567,
"FILES_IP" : "192.168.0.1",
"MONGODB_CONNECTION_URL" : "mongodb://127.0.0.1/genieacs",
"REDIS_PORT" : "6379",
"REDIS_HOST" : "127.0.0.1",
"CWMP_INTERFACE" : "0.0.0.0",
"CWMP_PORT" : 7547,
"CWMP_SSL" : false,
"NBI_INTERFACE" : "0.0.0.0",
"NBI_PORT" : 7557,
"FS_INTERFACE" : "0.0.0.0",
"FS_PORT" : 7567,
"FS_IP" : "192.168.0.1",
"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 PORT = 23;
var USERNAME = 'mt7109';
@@ -47,7 +69,7 @@ var telnetExecute = function(command, connection, prompt, callback) {
}
var getDeviceIp = function(deviceId, callback) {
var db = require('../../db');
var db = require('../../lib/db');
var URL = require('url');
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.
// 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');
@@ -68,7 +90,7 @@ var findFrequency = function(haystack, needle) {
}
var getDeviceIp = function(deviceId, callback) {
var db = require('../../db');
var db = require('../../lib/db');
var URL = require('url');
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-----
+78 -4
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'
db = require './db'
http = require 'http'
@@ -11,6 +51,17 @@ connectionRequest = (deviceId, callback) ->
# ensure socket is reused in case of digest authentication
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) ->
options = URL.parse(url)
options.agent = agent
@@ -19,7 +70,7 @@ connectionRequest = (deviceId, callback) ->
options.headers = {'Authorization' : authString}
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'])
callback(res.statusCode, authHeader)
# 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
callback(err)
return
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
#connectionRequestUrl = connectionRequestUrl.replace(/^(http:\/\/)([0-9\.]+)(\:[0-9]+\/[a-zA-Z0-9]+\/?$)/, '$110.1.1.254$3')
conReq(connectionRequestUrl, null, (statusCode, authHeader) ->
@@ -56,10 +117,10 @@ connectionRequest = (deviceId, callback) ->
authString = auth.digest(username, password, uri.path, 'GET', null, authHeader)
conReq(connectionRequestUrl, authString, (statusCode, authHeader) ->
callback()
callback(statusToError(statusCode))
)
else
callback()
callback(statusToError(statusCode))
)
)
@@ -169,8 +230,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.sanitizeTask = sanitizeTask
exports.connectionRequest = connectionRequest
exports.watchTask = watchTask
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'
@@ -7,11 +24,11 @@ exports.parseAuthHeader = (authHeader) ->
res['method'] = authHeader.slice(0, i)
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
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
res[r[1]] = r[2]
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
+38 -2
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]'
NULL_TYPE = '[object Null]'
BOOLEAN_TYPE = '[object Boolean]'
@@ -28,11 +53,11 @@ exports.arrayToHash = (arr) ->
return hash
exports.getDeviceId = (deviceIdStruct) ->
exports.generateDeviceId = (deviceIdStruct) ->
# Percent escaping function. Escapes everything except alphanumerics and underscore
esc = (str) ->
str.replace(/[^A-Za-z0-9_]/g, (chr) ->
buf = new require('buffer').Buffer(chr)
buf = new buffer.Buffer(chr, 'utf8')
rep = ''
rep += "%#{b.toString(16).toUpperCase()}" for b in buf
return rep
@@ -45,6 +70,17 @@ exports.getDeviceId = (deviceIdStruct) ->
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) ->
obj[name] = method for name, method of mixin
obj
+162 -36
View File
@@ -1,45 +1,171 @@
exports.DATABASE_NAME = 'genieacs'
exports.ACS_PORT = 7547 # CWMP port as assigned by IANA
exports.ACS_HTTPS_PORT = 7548
exports.ACS_INTERFACE = '0.0.0.0'
exports.ACS_HTTPS_INTERFACE = '0.0.0.0'
exports.API_PORT = 7557
exports.API_INTERFACE = '0.0.0.0'
exports.FILES_PORT = 7567
exports.FILES_INTERFACE = '0.0.0.0'
exports.FILES_IP = '172.240.21.2' # Used when sending download requests to devices
exports.CACHE_DURATION = 60 # in seconds
exports.PRESETS_CACHE_DURATION = 86400
exports.PRESETS_TIME_PADDING = 1
exports.MONGODB_SOCKET = '/tmp/mongodb-27017.sock'
exports.REDIS_SOCKET = '/tmp/redis.sock'
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
###
# 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/>.
###
exports.MONGODB_OPTIONS = {
db : {
w : 1
wtimeout : 60000
}
server : {
auto_reconnect : true
}
path = require 'path'
common = require './common'
options = {
CONFIG_DIR : {type : 'string', default : 'config'},
MONGODB_CONNECTION_URL : {type : 'string', default : 'mongodb://127.0.0.1/genieacs'},
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
c = require('../config/config')
for k, v of c
exports[k] = v
allConfig = {}
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
exports.PARAMETERS = require('../config/parameters')
exports.PARAMETERS = require(path.resolve(allConfig.CONFIG_DIR, 'parameters'))
# 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'
common = require './common'
+185 -219
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'
common = require './common'
util = require 'util'
http = require 'http'
https = require 'https'
soap = require './soap'
tasks = require './tasks'
normalize = require('./normalize').normalize
db = require './db'
presets = require './presets'
mongodb = require 'mongodb'
fs = require 'fs'
apiFunctions = require './api-functions'
customCommands = require './custom-commands'
crypto = require 'crypto'
parameters = require './parameters'
zlib = require 'zlib'
OBJECT_REGEX = /\.$/
INSTANCE_REGEX = /\.[\d]+\.$/
updateDevice = require './update-device'
# Used to reject new TR-069 sessions when under load
@@ -25,19 +58,14 @@ holdUntil = Date.now()
writeResponse = (currentRequest, res) ->
data = new Buffer(res.data)
if data.length == 0
# no more requests. terminate TCP connection
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) ->
if config.get('DEBUG', currentRequest.session.deviceId)
dump = "# RESPONSE #{new Date(Date.now())}\n" + JSON.stringify(res.headers) + "\n#{res.data}\n\n"
require('fs').appendFile("./debug/#{currentRequest.session.deviceId}.dump", dump, (err) ->
throw err if err
)
# 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']
when 'gzip'
res.headers['Content-Encoding'] = 'gzip'
@@ -47,95 +75,23 @@ writeResponse = (currentRequest, res) ->
compress = zlib.deflate
if compress?
compress(data, (err, data) ->
compress(res.data, (err, data) ->
res.headers['Content-Length'] = data.length
currentRequest.httpResponse.writeHead(res.code, res.headers)
currentRequest.httpResponse.end(data)
)
else
res.headers['Content-Length'] = data.length
res.headers['Content-Length'] = res.data.length
currentRequest.httpResponse.writeHead(res.code, res.headers)
currentRequest.httpResponse.end(data)
currentRequest.httpResponse.end(res.data)
updateDevice = (currentRequest, actions, callback) ->
if not actions?
callback() if callback?
return
now = new Date(Date.now())
updates = {}
deletes = {}
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
deletes["#{path}_orig"] = 1
else
updates["#{path}_orig"] = origValue
updates["#{path}_value"] = v
updates["#{path}_timestamp"] = now
if p[2]?
updates["#{path}_type"] = p[2]
else
deletes["#{path}_type"] = 1
if actions.deletedObjects?
for p in actions.deletedObjects
deletes[p] = 1
if actions.instanceName?
for i in actions.instanceName
updates["#{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])
updates["#{path}_instance"] = true
else
updates["#{path}_object"] = true
else
path = "#{p[0]}."
updates["#{path}_writable"] = p[1] if p[1]?
updates["#{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]
updates["_customCommands.#{commandName}._value"] = p[1]
updates["_customCommands.#{commandName}._timestamp"] = now
if actions.set?
common.extend(updates, actions.set)
if Object.keys(updates).length > 0 or Object.keys(deletes).length > 0
db.devicesCollection.update({'_id' : currentRequest.deviceId}, {'$set' : updates, '$unset' : deletes}, {}, (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) ->
endSession = (currentRequest) ->
db.redisClient.del("session_#{currentRequest.sessionId}", (err, res) ->
throw err if err
res = soap.response(null)
writeResponse(currentRequest, res)
)
break
)
callback?(err)
)
else
callback?()
inform = (currentRequest, cwmpRequest) ->
@@ -146,42 +102,49 @@ inform = (currentRequest, cwmpRequest) ->
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)
informHash = crypto.createHash('md5').update(JSON.stringify(parameterNames)).update(JSON.stringify(config.CUSTOM_COMMANDS)).digest('hex')
actions = {parameterValues : cwmpRequest.methodRequest.parameterList, set : {}}
actions.set._lastInform = now
actions.set._lastBoot = now if '1 BOOT' in cwmpRequest.methodRequest.event
deviceUpdates = {parameterValues : cwmpRequest.methodRequest.parameterList, informEvents : cwmpRequest.methodRequest.event}
# Delete all pending tasks on bootstrap (e.g. factory reset)
if '0 BOOTSTRAP' in cwmpRequest.methodRequest.event
actions.set._lastBootstrap = lastBootstrap = now
# ignore all pending tasks
db.tasksCollection.remove({device : currentRequest.deviceId}, (err, removed) ->
lastBootstrap = now
db.tasksCollection.remove({device : currentRequest.session.deviceId}, (err, removed) ->
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
updateAndRespond = () ->
updateDevice(currentRequest, actions, (err) ->
updateDevice.commitUpdates(currentRequest.session.deviceId, deviceUpdates, false, (err) ->
throw err if err
db.redisClient.setex("session_#{currentRequest.sessionId}", currentRequest.session.sessionTimeout, JSON.stringify(currentRequest.session), (err) ->
throw err if err
res = soap.response({
id : cwmpRequest.id,
methodResponse : {type : 'InformResponse'},
cwmpVersion : cwmpRequest.cwmpVersion,
cookies : {deviceId : currentRequest.deviceId}
cwmpVersion : cwmpRequest.cwmpVersion
})
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?
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
)
@@ -200,7 +163,7 @@ inform = (currentRequest, cwmpRequest) ->
path += p + '.'
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
lastBootstrap ?= device?._lastBootstrap
_tasks = []
@@ -210,7 +173,7 @@ inform = (currentRequest, cwmpRequest) ->
for k of reference
continue if k[0] == '_'
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})
else
traverse(reference[k], actual[k])
@@ -221,18 +184,18 @@ inform = (currentRequest, cwmpRequest) ->
deviceCustomCommands = {}
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)
_tasks.push({device: currentRequest.deviceId, name: 'customCommand', command: "#{cmd} init"})
_tasks.push({device: currentRequest.session.deviceId, name: 'customCommand', command: "#{cmd} init"})
delete deviceCustomCommands[cmd]
for cmd of deviceCustomCommands
actions.deletedObjects ?= []
actions.deletedObjects.push("_customCommands.#{cmd}")
deviceUpdates.deletedObjects ?= []
deviceUpdates.deletedObjects.push("_customCommands.#{cmd}")
# clear presets hash if parameters are potentially modified
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
)
@@ -244,13 +207,13 @@ inform = (currentRequest, cwmpRequest) ->
for k, v of cwmpRequest.methodRequest.deviceId
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
util.log("#{currentRequest.deviceId}: New device registered")
_tasks.push({device: currentRequest.deviceId, name : 'refreshObject', objectName : ''})
util.log("#{currentRequest.session.deviceId}: New device registered")
_tasks.push({device: currentRequest.session.deviceId, name : 'refreshObject', objectName : ''})
for cmd in customCommands.getDeviceCustomCommandNames(currentRequest.deviceId)
_tasks.push({device: currentRequest.deviceId, name: 'customCommand', command: "#{cmd} init"})
for cmd in customCommands.getDeviceCustomCommandNames(currentRequest.session.deviceId)
_tasks.push({device: currentRequest.session.deviceId, name: 'customCommand', command: "#{cmd} init"})
apiFunctions.insertTasks(_tasks, {}, () ->
updateAndRespond()
@@ -264,9 +227,6 @@ runTask = (currentRequest, task, methodResponse) ->
timeDiff = process.hrtime()
tasks.task(task, methodResponse, (err, status, methodRequest, deviceUpdates) ->
throw err if err
# TODO handle error
updateDevice(currentRequest, deviceUpdates, (err) ->
throw err if err
timeDiff = process.hrtime(timeDiff)[0] + 1
if timeDiff > 3 # in seconds
@@ -277,25 +237,33 @@ runTask = (currentRequest, task, methodResponse) ->
switch status & ~tasks.STATUS_SAVE
when tasks.STATUS_OK
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
res = soap.response({
id : task._id,
methodRequest : methodRequest,
cwmpVersion : currentRequest.cwmpVersion
cwmpVersion : currentRequest.session.cwmpVersion
})
writeResponse(currentRequest, res)
updateSessionExpiry(currentRequest)
)
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) ->
throw err if err
f()
)
)
else
updateDevice.queueUpdates(currentRequest.session.deviceId, deviceUpdates, (err) ->
f()
)
when tasks.STATUS_COMPLETED
util.log("#{currentRequest.deviceId}: Completed task #{task.name}(#{task._id})")
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) ->
throw err if err
db.redisClient.del(String(task._id), (err, res) ->
@@ -303,9 +271,10 @@ runTask = (currentRequest, task, methodResponse) ->
nextTask(currentRequest)
)
)
)
when tasks.STATUS_FAULT
retryAfter = config.RETRY_DELAY * Math.pow(2, task.retries ? 0)
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)
util.log("#{currentRequest.session.deviceId}: Fault response for task #{task._id}. Retrying after #{retryAfter} seconds.")
taskUpdate = {fault : task.fault, timestamp : new Date(Date.now() + retryAfter * 1000)}
if save
taskUpdate.session = task.session
@@ -317,7 +286,6 @@ runTask = (currentRequest, task, methodResponse) ->
else
throw new Error('Unknown task status')
)
)
isTaskExpired = (task) ->
@@ -325,41 +293,41 @@ isTaskExpired = (task) ->
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
devicePresetsHash = res[0]
presetsHash = res[1]
if devicePresetsHash? and devicePresetsHash == presetsHash
# no discrepancy, return empty response
res = soap.response(null)
writeResponse(currentRequest, res)
# no discrepancy, end the session
endSession(currentRequest)
else
PRESETS_CACHE_DURATION = config.get('PRESETS_CACHE_DURATION', currentRequest.session.deviceId)
db.getPresetsObjectsAliases((allPresets, allObjects, allAliases) ->
if not presetsHash?
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
)
presets.getDevicePreset(currentRequest.deviceId, allPresets, allObjects, allAliases, (devicePreset) ->
presets.processDevicePreset(currentRequest.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) ->
presets.getDevicePreset(currentRequest.session.deviceId, allPresets, allObjects, allAliases, (devicePreset) ->
presets.processDevicePreset(currentRequest.session.deviceId, devicePreset, (taskList, addTags, deleteTags, expiry) ->
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
)
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
util.log("#{currentRequest.deviceId}: Updating tags +(#{addTags}) -(#{deleteTags})")
util.log("#{currentRequest.session.deviceId}: Updating tags +(#{addTags}) -(#{deleteTags})")
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
)
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
)
@@ -368,12 +336,11 @@ assertPresets = (currentRequest) ->
apiFunctions.insertTasks(taskList, allAliases, (err, taskList) ->
throw err if err
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, {})
)
else
res = soap.response(null)
writeResponse(currentRequest, res)
endSession(currentRequest)
)
)
)
@@ -382,7 +349,7 @@ assertPresets = (currentRequest) ->
nextTask = (currentRequest) ->
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) ->
throw err if err
@@ -390,15 +357,43 @@ nextTask = (currentRequest) ->
# no more tasks, check presets discrepancy
assertPresets(currentRequest)
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) ->
throw err if err
nextTask(currentRequest)
)
else
util.log("#{currentRequest.deviceId}: Started task #{task.name}(#{task._id})")
util.log("#{currentRequest.session.deviceId}: Started task #{task.name}(#{task._id})")
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
)
)
listener = (httpRequest, httpResponse) ->
@@ -422,14 +417,13 @@ listener = (httpRequest, httpResponse) ->
chunks = []
bytes = 0
cookies = {}
stream.on('data', (chunk) ->
chunks.push(chunk)
bytes += chunk.length
)
httpRequest.getBody = (encoding) ->
httpRequest.getBody = () ->
# Write all chunks into a Buffer
body = new Buffer(bytes)
offset = 0
@@ -437,27 +431,38 @@ listener = (httpRequest, httpResponse) ->
chunk.copy(body, offset, 0, chunk.length)
offset += chunk.length
)
#Return encoded (default to UTF8) string
return body.toString(encoding || 'utf8', 0, body.byteLength)
return body
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 = {
httpRequest : httpRequest
httpResponse : httpResponse
cwmpVersion : cwmpRequest.cwmpVersion
deviceId = common.generateDeviceId(cwmpRequest.methodRequest.deviceId)
sessionId = crypto.randomBytes(8).toString('hex')
session = {
deviceId : deviceId,
cwmpVersion : cwmpRequest.cwmpVersion,
sessionTimeout : cwmpRequest.sessionTimeout ? config.get('SESSION_TIMEOUT', deviceId)
}
if cwmpRequest.methodRequest?.deviceId?
currentRequest.deviceId = common.getDeviceId(cwmpRequest.methodRequest.deviceId)
else
currentRequest.deviceId = cwmpRequest.cookies.deviceId
httpRequest.connection.setTimeout(session.sessionTimeout * 1000)
if config.DEBUG_DEVICES[currentRequest.deviceId]
currentRequest = {
httpRequest : httpRequest,
httpResponse : httpResponse,
sessionId : sessionId,
session : session
}
if config.get('DEBUG', currentRequest.session.deviceId)
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) ->
require('fs').appendFile("./debug/#{currentRequest.session.deviceId}.dump", dump, (err) ->
throw err if err
)
@@ -465,60 +470,53 @@ listener = (httpRequest, httpResponse) ->
if cwmpRequest.methodRequest.type is 'Inform'
inform(currentRequest, cwmpRequest)
else if cwmpRequest.methodRequest.type is 'GetRPCMethods'
util.log("#{currentRequest.deviceId}: GetRPCMethods")
util.log("#{currentRequest.session.deviceId}: GetRPCMethods")
res = soap.response({
id : cwmpRequest.id,
methodResponse : {type : 'GetRPCMethodsResponse', methodList : ['Inform', 'GetRPCMethods', 'TransferComplete', 'RequestDownload']},
cwmpVersion : currentRequest.cwmpVersion,
cookies : cookies
cwmpVersion : currentRequest.session.cwmpVersion
})
writeResponse(currentRequest, res)
updateSessionExpiry(currentRequest)
else if cwmpRequest.methodRequest.type is 'TransferComplete'
# do nothing
util.log("#{currentRequest.deviceId}: Transfer complete")
util.log("#{currentRequest.session.deviceId}: Transfer complete")
res = soap.response({
id : cwmpRequest.id,
methodResponse : {type : 'TransferCompleteResponse'},
cwmpVersion : currentRequest.cwmpVersion,
cookies : cookies
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.cwmpVersion,
cookies : cookies
cwmpVersion : currentRequest.session.cwmpVersion
})
writeResponse(currentRequest, res)
updateSessionExpiry(currentRequest)
fileType = cwmpRequest.methodRequest.fileType
util.log("#{currentRequest.deviceId}: RequestDownload (#{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) ->
presets.getDevicePreset(currentRequest.deviceId, allPresets, allObjects, allAliases, (devicePreset) ->
presets.getDevicePreset(currentRequest.session.deviceId, allPresets, allObjects, allAliases, (devicePreset) ->
presetSoftwareVersion = devicePreset.softwareVersion?.preset
currentSoftwareVersion = devicePreset.softwareVersion?.current._value
if presetSoftwareVersion? and presetSoftwareVersion != currentSoftwareVersion
projection = {
'_deviceId._Manufacturer' : 1,
'_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) ->
deviceIdStruct = common.parseDeviceId(currentRequest.session.deviceId)
db.filesCollection.findOne({'metadata.fileType' : '1 Firmware Upgrade Image', 'metadata.oui' : deviceIdStruct.oui, 'metadata.productClass' : deviceIdStruct.productClass, 'metadata.version' : presetSoftwareVersion}, {_id : 1}, (err, file) ->
throw err if err
if not file?
util.error("#{currentRequest.deviceId}: Firmware image not found (#{presetSoftwareVersion})")
util.error("#{currentRequest.session.deviceId}: Firmware image not found (#{presetSoftwareVersion})")
return requestDownloadResponse()
task = {
device : currentRequest.deviceId,
device : currentRequest.session.deviceId,
name : 'download',
file : file['_id']
}
@@ -527,7 +525,6 @@ listener = (httpRequest, httpResponse) ->
return requestDownloadResponse()
)
)
)
else
return requestDownloadResponse()
)
@@ -547,10 +544,8 @@ listener = (httpRequest, httpResponse) ->
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
# Fault not related to a task. End the session.
return endSession(currentRequest)
db.getTask(taskId, (err, task) ->
throw err if err
@@ -563,36 +558,7 @@ listener = (httpRequest, httpResponse) ->
# cpe sent empty response. start sending acs requests
nextTask(currentRequest)
)
cluster = require 'cluster'
numCPUs = require('os').cpus().length
if cluster.isMaster
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)
exports.listener = listener
+88 -13
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'
mongodb = require 'mongodb'
redisClient = require('redis').createClient(config.REDIS_SOCKET)
redis = require('redis')
parameters = require './parameters'
redisClient = null
tasksCollection = null
devicesCollection = null
presetsCollection = null
objectsCollection = null
mongodb.MongoClient.connect("mongodb://#{config.MONGODB_SOCKET}/#{config.DATABASE_NAME}", config.MONGODB_OPTIONS, (err, db) ->
# Only flush redis DB if aliases or custom commands config has changed
flushRedis = (callback) ->
redisClient.get('config_hash', (err, res) ->
return callback(err) if 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)
)
)
)
connect = (callback) ->
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) ->
throw new Error(err) if err?
exports.tasksCollection = tasksCollection = collection
collection.ensureIndex({device: 1, timestamp: 1}, (err) ->
collection?.ensureIndex({device: 1, timestamp: 1}, (err) ->
)
if --callbackCounter == 0 or err
callbackCounter = 0
return callback(err)
)
db.collection('devices', (err, collection) ->
throw new Error(err) if err?
exports.devicesCollection = devicesCollection = collection
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
if --callbackCounter == 0 or err
callbackCounter = 0
return callback(err)
)
db.collection('objects', (err, collection) ->
throw new Error(err) if err?
exports.objectsCollection = objectsCollection = collection
if --callbackCounter == 0 or err
callbackCounter = 0
return callback(err)
)
db.collection('fs.files', (err, collection) ->
throw new Error(err) if err?
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)
)
)
)
disconnect = () ->
exports.mongoDb.close()
redisClient.quit()
getTask = (taskId, callback) ->
@@ -100,7 +174,7 @@ getCached = (name, valueCallback, valueExpiry, 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
callback(res)
)
@@ -120,7 +194,7 @@ getPresetsObjectsAliases = (callback) ->
presetsCollection.find().toArray((err, res) ->
callback(err, res)
)
, config.PRESETS_CACHE_DURATION, (err, res) ->
, config.get('PRESETS_CACHE_DURATION'), (err, res) ->
throw err if err
presets = res
callback(presets, objects, aliases) if objects and aliases
@@ -134,14 +208,14 @@ getPresetsObjectsAliases = (callback) ->
objs[r._id] = r for r in res
callback(null, objs)
)
, config.PRESETS_CACHE_DURATION, (err, res) ->
, config.get('PRESETS_CACHE_DURATION'), (err, res) ->
throw err if err
objects = res
callback(presets, objects, aliases) if presets and 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
aliases = res
callback(presets, objects, aliases) if presets and objects
@@ -149,7 +223,8 @@ getPresetsObjectsAliases = (callback) ->
)
exports.redisClient = redisClient
exports.getTask = getTask
exports.getPresetsObjectsAliases = getPresetsObjectsAliases
exports.getAliases = getAliases
exports.connect = connect
exports.disconnect = disconnect
+27 -29
View File
@@ -1,39 +1,37 @@
###
# 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'
util = require 'util'
http = require 'http'
url = require 'url'
mongodb = require 'mongodb'
querystring = require 'querystring'
db = null
mongodb.MongoClient.connect("mongodb://#{config.MONGODB_SOCKET}/#{config.DATABASE_NAME}", config.MONGODB_OPTIONS, (err, _db) ->
db = _db
)
db = require './db'
cluster = require 'cluster'
numCPUs = require('os').cpus().length
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) ->
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) ->
listener = (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 = new mongodb.GridStore(db.mongoDb, filename, 'r', {})
gs.open((err, gs) ->
if err
response.writeHead(404)
@@ -46,6 +44,6 @@ else
else
response.writeHead(405, {'Allow': 'GET'})
response.end('405 Method Not Allowed')
)
server.listen config.FILES_PORT, config.FILES_INTERFACE
exports.listener = listener
+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'
@@ -93,6 +133,7 @@ optimizeProjection = (obj) ->
delete obj[b]
keys.splice(i--, 1)
++ i
return
exports.test = test
+68 -33
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'
util = require 'util'
http = require 'http'
url = require 'url'
common = require './common'
db = require './db'
@@ -16,31 +54,14 @@ TASKS_REGEX = /^\/tasks\/([a-zA-Z0-9\-\_\%]+)(\/[a-zA-Z_]*)?$/
TAGS_REGEX = /^\/devices\/([a-zA-Z0-9\-\_\%]+)\/tags\/([a-zA-Z0-9\-\_\%]+)\/?$/
PRESETS_REGEX = /^\/presets\/([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\-\_\.]+)\/?$/
QUERY_REGEX = /^\/([a-zA-Z0-9_]+s)\/?$/
DEVICE_PRESET_REGEX = /^\/devices\/([a-zA-Z0-9\-\_\%]+)\/preset\/?$/
DELETE_DEVICE_REGEX = /^\/devices\/([a-zA-Z0-9\-\_\%]+)\/?$/
cluster = require 'cluster'
numCPUs = require('os').cpus().length
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) ->
util.log("Worker #{worker.process.pid} died (#{worker.process.exitCode})")
setTimeout(()->
cluster.fork()
, config.WORKER_RESPAWN_TIME)
)
for [1 .. numCPUs]
cluster.fork()
else
server = http.createServer (request, response) ->
listener = (request, response) ->
chunks = []
bytes = 0
@@ -48,7 +69,7 @@ else
chunks.push(chunk)
bytes += chunk.length
request.getBody = (encoding) ->
request.getBody = () ->
# Write all chunks into a Buffer
body = new Buffer(bytes)
offset = 0
@@ -56,9 +77,7 @@ else
chunk.copy(body, offset, 0, chunk.length)
offset += chunk.length
)
# Return encoded (default to UTF8) string
return body.toString(encoding || 'utf8', 0, body.byteLength)
return body
request.addListener 'end', () ->
body = request.getBody()
@@ -161,7 +180,7 @@ else
else if DEVICE_TASKS_REGEX.test(urlParts.pathname)
if request.method == 'POST'
deviceId = querystring.unescape(DEVICE_TASKS_REGEX.exec(urlParts.pathname)[1])
if body
if body.length
task = JSON.parse(body)
task.device = deviceId
db.getAliases((aliases) ->
@@ -180,7 +199,7 @@ else
response.writeHead(202)
response.end()
else
apiFunctions.watchTask(task._id, config.DEVICE_ONLINE_THRESHOLD, (err) ->
apiFunctions.watchTask(task._id, config.get('DEVICE_ONLINE_THRESHOLD', deviceId), (err) ->
if err
response.writeHead(202)
response.end()
@@ -225,7 +244,7 @@ else
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])
action = r[2]
if not action? or action is '/'
if request.method == 'DELETE'
db.tasksCollection.remove({'_id' : taskId}, (err, removedCount) ->
@@ -257,22 +276,22 @@ else
if request.method == 'PUT'
metadata = {
fileType : request.headers.filetype,
manufacturer : request.headers.manufacturer,
oui : request.headers.oui,
productClass : request.headers.productclass,
version : request.headers.version,
}
gs = new mongodb.GridStore(db.mongoDb, filename, 'w', {metadata : metadata})
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
)
response.writeHead(201)
response.end()
)
)
)
else if request.method == 'DELETE'
mongodb.GridStore.unlink(db.mongoDb, filename, (err) ->
response.writeHead(200)
@@ -291,6 +310,21 @@ else
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(err.message)
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']
@@ -360,4 +394,5 @@ else
response.writeHead 404
response.end('404 Not Found')
server.listen config.API_PORT, config.API_INTERFACE
exports.listener = listener
+32 -9
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.
###
# 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'
parameters = require './parameters'
@@ -8,20 +33,18 @@ stringToRegexp = (input) ->
output = input.replace(/[\[\]\\\^\$\.\|\?\+\(\)]/, "\\$&")
if output[0] == '*'
prefix = '/'
output = output.replace(/^\*+/g, '')
else
prefix = '/^'
output = '^' + output
if output[output.length - 1] == '*'
suffix = '/'
output = output.replace(/\*+$/g, '')
else
suffix = '$/'
output = output + '$'
output = output.replace(/[\*]/, '.*')
return eval(prefix + output + suffix)
return new RegExp(output)
normalizers = {}
@@ -66,9 +89,9 @@ normalizers.date = (input, normType) ->
normalizers.string = (input, normType) ->
if normType is 'query'
if /^\/(.*?)\/(g?i?m?y?)$/.test(input)
return [{'$regex' : eval(input)}, input]
input
if (m = /^\/(.*?)\/(g?i?m?y?)$/.exec(input))
return [{'$regex' : new RegExp(m[1], m[2])}, input]
return input
colonizeMac = (input) ->
@@ -101,7 +124,7 @@ normalizers.mac = (input, normType) ->
if input.length == 17
return input
return {'$regex' : eval('/' + input + '/')}
return {'$regex' : new RegExp(input)}
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'
db = require './db'
@@ -160,7 +177,7 @@ compileAliases = (callback) ->
counter = 0
splitIds(4, (batches) ->
if not batches?
return callback(null, aliases, config.ALIASES_CACHE)
return callback(null, aliases)
for batch in batches
options = {
@@ -184,7 +201,7 @@ compileAliases = (callback) ->
aliases[alias].push(id)
if ++counter == batches.length
callback?(null, aliases, config.ALIASES_CACHE)
callback?(null, aliases)
)
return
)
+53 -4
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'
common = require './common'
db = require './db'
@@ -28,7 +68,11 @@ exports.getDevicePreset = (deviceId, presets, objects, aliases, callback) ->
# only fetch relevant params
projection = {_id : 1}
for p in presets
p.precondition = query.expand(p.precondition, aliases)
if common.typeOf(p.precondition) is common.STRING_TYPE
p.precondition = query.expand(JSON.parse(p.precondition), aliases)
else
# Accept an object for backward compatiblity
p.precondition = query.expand(p.precondition ? {}, aliases)
mongoQuery.projection(p.precondition, projection)
for c in p.configurations
@@ -40,6 +84,7 @@ exports.getDevicePreset = (deviceId, presets, objects, aliases, callback) ->
if commandfile of deviceCustomCommands
projection["_customCommands.#{c.command.split(' ', 1)}"] = 1
when 'software_version'
projection['Device.DeviceInfo.SoftwareVersion'] = 1
projection['InternetGatewayDevice.DeviceInfo.SoftwareVersion'] = 1
when 'add_tag', 'delete_tag'
projection['_tags'] = 1
@@ -160,6 +205,9 @@ exports.getDevicePreset = (deviceId, presets, objects, aliases, callback) ->
when 'software_version'
devicePreset.softwareVersion ?= {}
devicePreset.softwareVersion.preset = c.software_version
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
@@ -170,8 +218,9 @@ exports.getDevicePreset = (deviceId, presets, objects, aliases, callback) ->
exports.processDevicePreset = (deviceId, devicePreset, callback) ->
PRESETS_TIME_PADDING = config.get('PRESETS_TIME_PADDING')
now = new Date()
expiry = config.PRESETS_CACHE_DURATION
expiry = config.get('PRESETS_CACHE_DURATION', deviceId)
getParameterValues = []
setParameterValues = []
addTags = []
@@ -188,7 +237,7 @@ exports.processDevicePreset = (deviceId, devicePreset, callback) ->
setParameterValues.push([parameterPath, presetValue, parameterDetails.current._type])
else if parameterDetails.preset.expiry?
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?
getParameterValues.push(parameterPath)
else
@@ -236,7 +285,7 @@ exports.processDevicePreset = (deviceId, devicePreset, callback) ->
taskList.push({device : deviceId, name : 'customCommand', command : commandDetails.preset.valueCommand})
else if commandDetails.preset.expiry?
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})
else
expiry = Math.min(expiry, diff)
+22
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.
###
common = require './common'
normalize = require './normalize'
+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()
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'
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
if config.IGNORE_XML_NAMESPACES
if config.get('IGNORE_XML_NAMESPACES')
libxmljs.Element.prototype.__find = libxmljs.Element.prototype.find
libxmljs.Element.prototype.find = (xpath, namespaces) ->
# 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)
# 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) ->
e.text().trim() for e in xml.find('EventStruct/EventCode')
@@ -205,7 +232,7 @@ acsInform = (xml) ->
parameterList : parameterValueList(xml.get('ParameterList')),
deviceId : traverseXml(xml.get('DeviceId')),
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)
exports.request = (httpRequest) ->
cwmpRequest = {cookies: {}}
cwmpRequest.cookies = cookiesToObj(httpRequest.headers.cookie) if httpRequest.headers.cookie
cwmpRequest.cwmpVersion = cwmpRequest.cookies.cwmpVersion
exports.request = (httpRequest, cwmpVersion) ->
cwmpRequest = {cwmpVersion : cwmpVersion}
data = httpRequest.getBody()
if data.length > 0
try
xml = libxmljs.parseXml data
catch err
# some devices send invalid utf8 characters
xml = libxmljs.parseXml httpRequest.getBody('binary')
xml = libxmljs.parseXml(data, LIBXMLJS_OPTIONS)
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'])
switch methodElement.namespace().href()
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
if methodElement?
if methodElement? and not (config.get('IGNORE_XML_NAMESPACES') and methodElement.name() is 'Fault')
switch methodElement.name()
when 'Inform'
cwmpRequest.methodRequest = acsInform(methodElement)
@@ -360,7 +381,6 @@ createSoapEnv = (cwmpVersion) ->
exports.response = (cwmpResponse) ->
headers = {
'Content-Type' : 'text/xml; charset="utf-8"',
'Server' : SERVER_NAME,
'SOAPServer' : SERVER_NAME,
}
@@ -372,8 +392,6 @@ exports.response = (cwmpResponse) ->
body = env.node('soap-env:Body')
switch cwmpResponse.methodResponse.type
when 'InformResponse'
cwmpResponse.cookies ?= {}
cwmpResponse.cookies.cwmpVersion = cwmpResponse.cwmpVersion
acsInformResponse(body, cwmpResponse.methodResponse)
when 'GetRPCMethodsResponse'
acsGetRPCMethodsResponse(body, cwmpResponse.methodResponse)
@@ -408,10 +426,8 @@ exports.response = (cwmpResponse) ->
else
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?
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
return {code: 204, headers: headers, data: ''}
return {code: 204, headers: headers, data: new Buffer(0)}
+92 -73
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'
common = require './common'
db = require './db'
mongodb = require 'mongodb'
customCommands = require './custom-commands'
BATCH_SIZE = 16
url = require 'url'
exports.STATUS_OK = STATUS_OK = 1
exports.STATUS_FAULT = STATUS_FAULT = 2
@@ -17,7 +57,7 @@ this.refreshObject = (task, methodResponse, callback) ->
if not task.session.subtask?
path = task.objectName
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?
task.fault = methodResponse
@@ -26,12 +66,16 @@ this.refreshObject = (task, methodResponse, callback) ->
if task.session.subtask.name is 'getParameterNames'
this.getParameterNames(task.session.subtask, methodResponse, (err, status, methodRequest, deviceUpdates) =>
if status & STATUS_COMPLETED
parameterNames = []
task.session.parameterNames ?= []
if deviceUpdates?.parameterNames?
for p in deviceUpdates.parameterNames
if not common.endsWith(p[0], '.')
parameterNames.push(p[0])
task.session.subtask = {name : 'getParameterValues', parameterNames : parameterNames}
task.session.parameterNames.push(p[0])
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) ->
# ignore deviceUpdates returned by firt call to getParameterValues
callback(err, status, methodRequest, deviceUpdates)
@@ -39,7 +83,7 @@ this.refreshObject = (task, methodResponse, callback) ->
else if status & STATUS_OK
callback(err, STATUS_OK, methodRequest, deviceUpdates)
else
throw Error('Unexpected subtask status')
throw new Error('Unexpected subtask status')
)
else if task.session.subtask.name is 'getParameterValues'
if methodResponse.faultcode?
@@ -53,88 +97,55 @@ this.refreshObject = (task, methodResponse, callback) ->
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) ->
if methodResponse.faultcode?
task.fault = methodResponse
callback(null, STATUS_FAULT)
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'
# Make sure that for each parameters, all its parents are included
found = {}
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[path] = 0 if !!path
for p in deviceUpdates.parameterNames
param = p[0]
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)
break if found[pp]?
found[pp] = 0
found[p[0]] = 1
for k, v of found
methodResponse.parameterList.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)
deviceUpdates.parameterNames.push([k]) if v == 0
else
# avoid adding and deleting the same param
if rootPath isnt task.parameterPath
deviceUpdates.deletedObjects = [rootPath.slice(0, -1)]
task.session = {queue : [task.parameterPath]}
# 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
if task.session.queue.length > 0
path = task.session.queue[-1..][0]
methodRequest = {
type : 'GetParameterNames',
parameterPath : task.parameterPath,
nextLevel : if task.nextLevel? then task.nextLevel else false
parameterPath : path,
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) ->
@@ -150,7 +161,7 @@ this.getParameterValues = (task, methodResponse, callback) ->
else if methodResponse.parameterList?
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)
if methodResponse.type is 'GetParameterValuesResponse'
@@ -180,7 +191,7 @@ this.setParameterValues = (task, methodResponse, callback) ->
prevValues = task.parameterValues.slice(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)
if prevValues?
@@ -206,6 +217,7 @@ this.addObject = (task, methodResponse, callback) ->
if methodResponse.type is 'AddObjectResponse'
status_save = STATUS_SAVE
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.appliedParameterValues = []
task.session.parameterNames = []
@@ -224,7 +236,7 @@ this.addObject = (task, methodResponse, callback) ->
task.session.parameterNames.push(p[0]) if not common.endsWith(p[0], '.')
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()
else if status & STATUS_OK
# Use STATUS_SAVE to avoid adding duplicate object in case of error
@@ -251,7 +263,7 @@ this.addObject = (task, methodResponse, callback) ->
return callback(null, STATUS_FAULT)
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()
else
callback(err, status, methodRequest, allDeviceUpdates)
@@ -319,11 +331,18 @@ this.download = (task, methodResponse, callback) ->
callback(err)
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 = {
type : 'Download',
fileType : file.metadata.fileType,
fileSize : file.length,
url : "http://#{config.FILES_IP}:#{config.FILES_PORT}/#{encodeURIComponent(file.filename)}",
url : url.format(l),
successUrl : task.successUrl,
failureUrl : task.failureUrl
}
+221
View File
@@ -0,0 +1,221 @@
###
# 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.instanceName?
for i in updatesBatch.instanceName
update['$set']["#{i[0]}._name"] = i[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
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "genieacs",
"version": "0.9.8",
"version": "1.0.3",
"description": "A blazing fast TR-069 auto configuration server (ACS) built with Node.js, Redis, and MongoDB",
"repository": {
"type": "git",
@@ -20,10 +20,10 @@
"bin": "bin"
},
"dependencies": {
"libxmljs": "~0.8.1",
"redis": "~0.10.0",
"hiredis": "~0.1.15",
"mongodb": "~1.3.23"
"libxmljs": "~0.11.0",
"redis": "~0.12.1",
"hiredis": "~0.1.17",
"mongodb": "~1.4.8"
},
"devDependencies": {
"coffee-script": "~1.7.1"
@@ -34,7 +34,7 @@
"scripts": {
"prepublish": "npm run compile",
"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",
"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'
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'
libxmljs = require 'libxmljs'
deviceTemplate = require './huawei_bm632w'