diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0b5d77f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*~ +*.js +node_modules diff --git a/acs.coffee b/acs.coffee index 4b66085..6821ccc 100644 --- a/acs.coffee +++ b/acs.coffee @@ -102,6 +102,12 @@ updateDevice = (currentRequest, actions, callback) -> updates["#{path}_writable"] = p[1] if p[1]? updates["#{path}_timestamp"] = now + if actions.customCommands? + for p in actions.customCommands + commandName = p[0] + updates["_customCommands.#{commandName}._value"] = p[1] + updates["_customCommands.#{commandName}._timestamp"] = now + if Object.keys(updates).length > 0 or Object.keys(deletes).length > 0 db.devicesCollection.update({'_id' : currentRequest.deviceId}, {'$set' : updates, '$unset' : deletes}, {}, (err, count) -> if (err) @@ -174,6 +180,12 @@ runTask = (currentRequest, task, methodResponse) -> res = tr069.response(task._id, cwmpResponse) writeResponse(currentRequest, res) ) + when tasks.STATUS_SAVE + db.saveTask(task, (err) -> + throw new Error(err) if err? + res = tr069.response(task._id, cwmpResponse) + writeResponse(currentRequest, res) + ) else throw Error('Unknown task status') ) diff --git a/config.coffee b/config.coffee index 5c9e453..4b2781c 100644 --- a/config.coffee +++ b/config.coffee @@ -49,3 +49,5 @@ exports.ALIASES = aliases # load authentication scripts exports.auth = require('./config/auth') + +exports.CUSTOM_COMMANDS = require('./config/custom_commands') diff --git a/config/custom_commands-sample.json b/config/custom_commands-sample.json new file mode 100644 index 0000000..b9200a0 --- /dev/null +++ b/config/custom_commands-sample.json @@ -0,0 +1,3 @@ +{ + "seowon_frequencies" : "/^[0-9]+\\-SWU\\%2D9200\\-[0-9A-Z]+$/" +} diff --git a/config/custom_commands/seowon_frequencies.js b/config/custom_commands/seowon_frequencies.js new file mode 100644 index 0000000..e39554c --- /dev/null +++ b/config/custom_commands/seowon_frequencies.js @@ -0,0 +1,199 @@ +// 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'); +var PORT = 23; +var USERNAME = 'admin'; +var PASSWORD = 'admin'; + +var FREQUENCIES_REGEX = /\s*([\d]+)\.\s*Frequency\s*:\s*([\d]+)\s\(KHz\),\s*Bandwidth\s*:\s*([\d\.]+)\s*\(MHz\)/gm + +var telnetConnect = function(host, callback) { + var client = net.connect(PORT, host, function() { + // enter username + telnetExecute(USERNAME, client, "Password: ", function(err, response) { + if (err) return callback(err, client); + if (!timeout) return; + // enter password + telnetExecute(PASSWORD, client, "MT7109> ", function(err, response) { + if (!timeout) return; + clearTimeout(timeout); + return callback(err, client); + }); + }); + }); + var timeout = setTimeout(function() { + timeout = null; + return callback('timeout', client); + }, 4000); +} + +var telnetExecute = function(command, connection, prompt, callback) { + command += "\n"; + var response = ''; + var listener = function(chunk) { + response += chunk.toString(); + if (response.slice(0 - prompt.length) == prompt) { + if (!timeout) return; + clearTimeout(timeout); + connection.removeListener('data', listener); + return callback(null, response.slice(command.length, 0 - prompt.length)); + } + }; + + var end = function() { + if (!timeout) return; + clearTimeout(timeout); + connection.removeListener('end', end); + return callback(null, response.slice(command.length)); + }; + + var timeout = setTimeout(function() { + timeout = null; + return callback('timeout', response); + }, 2000); + + if (prompt) + connection.on('data', listener); + else + connection.on('end', end); + + if (command) + connection.write(command); +} + +var parseFrequencies = function(frequenciesString) { + var a = frequenciesString.split(',') + var l = []; + for (i in a) { + var sp = a[i].trim().split(' '); + var frequency = parseInt(sp[0]); + var bandwidth = parseFloat(sp[1]); + l.push([frequency, bandwidth]); + } + return l; +} + +var findFrequency = function(haystack, needle) { + for (i in haystack) { + if (haystack[i][0] == needle[0] && haystack[i][1] == needle[1]) + return i; + } + return -1; +} + +var getDeviceIp = function(deviceId, callback) { + var db = require('../../db'); + var URL = require('url'); + + db.devicesCollection.findOne({_id : deviceId}, {'InternetGatewayDevice.ManagementServer.ConnectionRequestURL._value' : 1}, function(err, device) { + var connectionRequestUrl = device.InternetGatewayDevice.ManagementServer.ConnectionRequestURL._value; + var url = URL.parse(connectionRequestUrl); + return callback(url.hostname); + }); +}; + +exports.init = function(deviceId, args, callback) { + return exports.get(deviceId, args, callback); +}; + +exports.get = function(deviceId, args, callback) { + var currentFrequencies = []; + + getDeviceIp(deviceId, function(ip) { + telnetConnect(ip, function(err, client) { + // enter privileged mode + telnetExecute("enable", client, "MT7109# ", function(err, response) { + if (err) return callback(err); + // show frequencies + telnetExecute("show wmx freq", client, "MT7109# ", function(err, response) { + if (err) return callback(err); + while (f = FREQUENCIES_REGEX.exec(response)) { + var frequency = parseInt(f[2]); + var bandwidth = Math.floor(parseFloat(f[3]) * 1000); + var freqStr = String(frequency) + ' ' + String(bandwidth); + currentFrequencies.push(freqStr); + } + // log out + telnetExecute("logout", client, null, function(err, response) { + return callback(err, currentFrequencies.join(',')); + }); + }); + }); + }); + }); +}; + +exports.set = function(deviceId, args, callback) { + var newFrequencies = parseFrequencies(args); + var addFrequencies = []; + var delFrequencies = []; + + getDeviceIp(deviceId, function(ip) { + telnetConnect(ip, function(err, client) { + // enter privileged mode + telnetExecute("enable", client, "MT7109# ", function(err, response) { + if (err) return callback(err); + // show frequencies + telnetExecute("show wmx freq", client, "MT7109# ", function(err, response) { + if (err) return callback(err); + while (f = FREQUENCIES_REGEX.exec(response)) { + var index = parseInt(f[1]); + var frequency = parseInt(f[2]); + var bandwidth = Math.floor(parseFloat(f[3])); + + var i = findFrequency(newFrequencies, [frequency, bandwidth * 1000]); + if (i == -1) + delFrequencies.push(index); + else + newFrequencies.splice(i, 1); + } + for (f in newFrequencies) + addFrequencies.push(newFrequencies[f]); + + if (addFrequencies.length == 0 && delFrequencies.length == 0) { + // log out + telnetExecute("logout", client, null, function(err, response) { + return callback(err, args); + }); + return; + } + + telnetExecute("wimax", client, "MT7109 (WiMax)# ", function(err, response) { + if (err) return callback(err); + var add, del; + del = function() { + if (delFrequencies.length == 0) { + add(); + return; + } + var i = delFrequencies.pop(); + telnetExecute("wmx freq del " + i, client, "MT7109 (WiMax)# ", function(err, response) { + if (err) return callback(err); + del(); + }); + }; + add = function() { + if (addFrequencies.length == 0) { + // commit + telnetExecute("commit", client, "MT7109 (WiMax)# ", function(err, response) { + if (err) return callback(err); + // log out + telnetExecute("logout", client, null, function(err, response) { + return callback(null, args); + }); + }); + return; + } + var i = addFrequencies.pop(); + telnetExecute("wmx freq add " + i[0] + ' ' + i[1]/1000, client, "MT7109 (WiMax)# ", function(err, response) { + if (err) return callback(err); + add(); + }); + }; + del(); + }); + }); + }); + }); + }); +}; diff --git a/custom-commands.coffee b/custom-commands.coffee new file mode 100644 index 0000000..314fa03 --- /dev/null +++ b/custom-commands.coffee @@ -0,0 +1,27 @@ +config = require './config' +common = require './common' + + +exports.execute = (deviceId, command, callback) -> + args = command.split(/\s+/) + f = args.shift() + c = args.shift() + + file = require "./config/custom_commands/#{f}" + file[c](deviceId, args.join(' '), callback) + + +exports.getFileCommands = (filename) -> + commands = [] + f = require("./config/custom_commands/#{filename}") + for k,v of f + commands.push(k) + return commands + + +exports.getDeviceCustomCommands = (deviceId) -> + commands = {} + for k,v of config.CUSTOM_COMMANDS + if eval(v).test(deviceId) + commands[k] = exports.getFileCommands(k) + return commands diff --git a/db.coffee b/db.coffee index be9d80d..c93c2be 100644 --- a/db.coffee +++ b/db.coffee @@ -72,9 +72,13 @@ updateTask = (task, callback) -> saveTask = (task, callback) -> - task._id = mongodb.ObjectID(String(task._id)) - tasksCollection.save(task, (err) -> - callback(err) + # task ID can either be a string or a MongoDB ID + id = String(task._id) + task._id = mongodb.ObjectID(id) + tasksCollection.save(task, (mongoErr) -> + memcached.del(id, (memcachedErr, res) -> + callback(mongoErr or memcachedErr) + ) ) diff --git a/presets.coffee b/presets.coffee index 10b12e1..9d62c0c 100644 --- a/presets.coffee +++ b/presets.coffee @@ -4,6 +4,7 @@ db = require './db' mongoQuery = require './mongo-query' query = require './query' log = require('util').log +customCommands = require './custom-commands' getPresets = (callback) -> @@ -65,6 +66,7 @@ matchObject = (object, param) -> exports.assertPresets = (deviceId, presetsHash, callback) -> getPresets((presets, objects) -> + deviceCustomCommands = customCommands.getDeviceCustomCommands(deviceId) # only fetch relevant params projection = {_id : 1} for p in presets @@ -75,6 +77,10 @@ exports.assertPresets = (deviceId, presetsHash, callback) -> switch c.type when 'value', 'age' projection[c.name] = 1 + when 'custom_command_value', 'custom_command_age' + commandfile = c.command.split(' ', 1) + if commandfile of deviceCustomCommands + projection["_customCommands.#{c.command.split(' ', 1)}"] = 1 when 'firmware' projection['InternetGatewayDevice.DeviceInfo.SoftwareVersion'] = 1 when 'add_tag', 'delete_tag' @@ -120,6 +126,23 @@ exports.assertPresets = (deviceId, presetsHash, callback) -> getParameterValues.push(c.name) else expiry = Math.min(expiry, c.age - timeDiff) + when 'custom_command_value' + [filename, commandName] = c.command.split(' ', 2) + continue if not deviceCustomCommands[filename]? or commandName not in deviceCustomCommands[filename] + currentValue = try common.getParamValueFromPath(device, "_customCommands.#{filename}")._value catch then undefined + dst = common.matchType(currentValue, c.value) + if currentValue != dst + taskList.push({device : deviceId, name : 'customCommand', command : c.command}) + when 'custom_command_age' + [filename, commandName] = c.command.split(' ', 2) + continue if not deviceCustomCommands[filename]? or commandName not in deviceCustomCommands[filename] + command = common.getParamValueFromPath(device, "_customCommands.#{filename}") + timeDiff = try (now - command._timestamp) / 1000 catch then 9999999 + if (c.age - timeDiff < config.PRESETS_TIME_PADDING) + expiry = Math.min(expiry, c.age) + taskList.push({device : deviceId, name : 'customCommand', command : c.command}) + else + expiry = Math.min(expiry, c.age - timeDiff) when 'add_tag' add_tags.push(c.tag) if not device['_tags']? or c.tag not in device['_tags'] when 'delete_tag' @@ -200,6 +223,7 @@ getObjectHash = (object) -> hash += "#{k}=#{object[k]}" return hash + accumulateConfigurations = (presets, objects) -> maxWeights = {} configurations = {} diff --git a/tasks.coffee b/tasks.coffee index 3a34cc6..cd938dd 100644 --- a/tasks.coffee +++ b/tasks.coffee @@ -2,13 +2,28 @@ config = require './config' common = require './common' db = require './db' mongodb = require 'mongodb' +customCommands = require './custom-commands' BATCH_SIZE = 16 exports.STATUS_QUEUED = STATUS_QUEUED = 0 exports.STATUS_STARTED = STATUS_STARTED = 1 -exports.STATUS_PENDING = STATUS_PENDING = 2 -exports.STATUS_FAULT = STATUS_FAULT = 3 -exports.STATUS_FINISHED = STATUS_FINISHED = 4 +exports.STATUS_SAVE = STATUS_SAVE = 2 +exports.STATUS_PENDING = STATUS_PENDING = 3 +exports.STATUS_FAULT = STATUS_FAULT = 4 +exports.STATUS_FINISHED = STATUS_FINISHED = 5 + + +initCustomCommands = (deviceId, callback) -> + updates = {customCommands : []} + counter = 0 + for k,v of customCommands.getDeviceCustomCommands(deviceId) + if 'init' in v + ++counter + customCommands.execute(deviceId, "#{k} init", (err, value) -> + updates.customCommands.push([k, value]) + callback(updates) if --counter <= 0 + ) + callback(updates) if counter <= 0 this.init = (task, methodResponse, callback) -> @@ -16,7 +31,7 @@ this.init = (task, methodResponse, callback) -> task.subtask = {device : task.device, name : 'getParameterNames', parameterPath : '', nextLevel : false} if task.subtask.name == 'getParameterNames' - if methodResponse.faultcode? + if methodResponse.faultcode? task.fault = methodResponse callback(null, STATUS_FAULT) return @@ -42,7 +57,13 @@ this.init = (task, methodResponse, callback) -> # Ignore GetParameterValues errors. A workaround for the crappy Seewon devices. methodResponse = {parameterList : {}} this.getParameterValues(task.subtask, methodResponse, (err, status, cwmpResponse, deviceUpdates) -> - callback(err, status, cwmpResponse, deviceUpdates) + if status is STATUS_FINISHED + initCustomCommands(task.device, (updates) -> + common.extend(deviceUpdates, updates) + callback(err, status, cwmpResponse, deviceUpdates) + ) + else + callback(err, status, cwmpResponse, deviceUpdates) ) else throw Error('Unexpected subtask name') @@ -241,7 +262,11 @@ this.addObject = (task, methodResponse, callback) -> task.subtask = {name : 'getParameterValues', parameterNames : task.parameterNames} subtask() else if status is STATUS_STARTED - callback(err, STATUS_STARTED, cwmpResponse, allDeviceUpdates) + if allDeviceUpdates.instanceName? + # Use STATUSS_SAVE to avoid adding duplicate object in case of error + callback(err, STATUS_SAVE, cwmpResponse, allDeviceUpdates) + else + callback(err, STATUS_STARTED, cwmpResponse, allDeviceUpdates) else throw Error('Unexpected subtask status') ) @@ -343,5 +368,17 @@ this.download = (task, methodResponse, callback) -> callback(null, STATUS_FINISHED) +this.customCommand = (task, methodResponse, callback) -> + # TODO implement timeout + customCommands.execute(task.device, task.command, (err, value) -> + if err? + task.fault = err + callback(null, STATUS_FAULT) + else + commandName = task.command.split(' ', 2)[0] + callback(null, STATUS_FINISHED, null, {customCommands : [[commandName, value]]}) + ) + + exports.task = (task, methodResponse, callback) -> this[task.name](task, methodResponse, callback)