From 462e9e3afdfb72e2f1c4a361fabae1045e9aceae Mon Sep 17 00:00:00 2001 From: Zaid Abdulla Date: Tue, 22 Jan 2013 11:48:24 +0300 Subject: [PATCH] Major updates paving the road support ACS methods --- acs.coffee | 77 ++++++++------- tasks.coffee | 52 ++++++---- tr-069.coffee | 269 +++++++++++++++++++++++++------------------------- 3 files changed, 207 insertions(+), 191 deletions(-) diff --git a/acs.coffee b/acs.coffee index b616fc5..b5c0da5 100644 --- a/acs.coffee +++ b/acs.coffee @@ -21,7 +21,7 @@ applyConfigurations = (currentRequest, taskList) -> runTask(currentRequest, task, {}) ) else - res = tr069.response(currentRequest.sessionId, {}, {}) + res = tr069.response(null, {}, {}) writeResponse(currentRequest, res) @@ -91,14 +91,15 @@ updateDevice = (currentRequest, actions, callback) -> callback() if callback? -runTask = (currentRequest, task, cwmpRequest) -> +runTask = (currentRequest, task, methodResponse) -> cwmpResponse = {} - actions = tasks.task(task, cwmpRequest, cwmpResponse) + actions = tasks.task(task, methodResponse, cwmpResponse) + updateDevice(currentRequest, actions) if Object.keys(cwmpResponse).length > 0 db.updateTask(task, (err) -> - res = tr069.response(currentRequest.sessionId, cwmpResponse, {task : String(task._id)}) + res = tr069.response(task._id, cwmpResponse) writeResponse(currentRequest, res) ) return @@ -107,7 +108,7 @@ runTask = (currentRequest, task, cwmpRequest) -> db.memcached.del(String(task._id)) db.tasksCollection.remove({'_id' : mongodb.ObjectID(String(task._id))}, {safe: true}, (err, removed) -> util.log("#{currentRequest.deviceId}: Completed task #{task.name}(#{task._id})") - cookies = {task : null} + cookies = {} nextTask(currentRequest, cookies) ) @@ -115,7 +116,6 @@ runTask = (currentRequest, task, cwmpRequest) -> nextTask = (currentRequest, cookies) -> cur = db.tasksCollection.find({'device' : currentRequest.deviceId}).sort(['timestamp']).limit(1) cur.nextObject( (err, task) -> - cwmpRequest = {} cwmpResponse = {} if not task @@ -136,22 +136,22 @@ nextTask = (currentRequest, cookies) -> ) else # no discrepancy, return empty response - res = tr069.response(currentRequest.sessionId, cwmpResponse, cookies) + res = tr069.response(null, cwmpResponse, cookies) writeResponse(currentRequest, res) ) else # no discrepancy, return empty response - res = tr069.response(currentRequest.sessionId, cwmpResponse, cookies) + res = tr069.response(null, cwmpResponse, cookies) writeResponse(currentRequest, res) ) return else if task.fault? # last task was faulty. Do nothing until until task is deleted - res = tr069.response(currentRequest.sessionId, cwmpResponse, cookies) + res = tr069.response(null, cwmpResponse, cookies) writeResponse(currentRequest, res) else util.log("#{currentRequest.deviceId}: Started task #{task.name}(#{task._id})") - runTask(currentRequest, task, cwmpRequest) + runTask(currentRequest, task, {}) ) @@ -209,11 +209,10 @@ else cwmpRequest = tr069.request(httpRequest) # get deviceId either from inform xml or cookie - if cwmpRequest.deviceId? - currentRequest.deviceId = common.getDeviceId(cwmpRequest.deviceId) - cookies.DeviceId = currentRequest.deviceId + if cwmpRequest.methodRequest? and cwmpRequest.methodRequest.type is 'Inform' + cookies.deviceId = currentRequest.deviceId = common.getDeviceId(cwmpRequest.methodRequest.deviceId) else - currentRequest.deviceId = cwmpRequest.cookies.DeviceId + currentRequest.deviceId = cwmpRequest.cookies.deviceId if config.DEBUG_DEVICES[currentRequest.deviceId] dump = "# REQUEST #{new Date(Date.now())}\n" + JSON.stringify(httpRequest.headers) + "\n#{httpRequest.getBody()}\n\n" @@ -221,38 +220,40 @@ else throw new Error(err) if err ) - if cwmpRequest.inform - cwmpResponse.inform = true - cookies.ID = currentRequest.sessionId = cwmpRequest.sessionId + if cwmpRequest.methodRequest? + if cwmpRequest.methodRequest.type == 'Inform' + cwmpResponse.methodResponse = {type : 'InformResponse'} - if config.LOG_INFORMS - util.log("#{currentRequest.deviceId}: Inform (#{cwmpRequest.eventCodes}); retry count #{cwmpRequest.retryCount}") + if config.LOG_INFORMS + util.log("#{currentRequest.deviceId}: Inform (#{cwmpRequest.methodRequest.event}); retry count #{cwmpRequest.methodRequest.retryCount}") - updateDevice(currentRequest, {'inform' : true, 'parameterValues' : cwmpRequest.informParameterValues}, (err) -> - res = tr069.response(currentRequest.sessionId, cwmpResponse, cookies) - writeResponse(currentRequest, res) - ) - return + updateDevice(currentRequest, {'inform' : true, 'parameterValues' : cwmpRequest.methodRequest.parameterList}, (err) -> + res = tr069.response(cwmpRequest.id, cwmpResponse, cookies) + writeResponse(currentRequest, res) + ) + else + throw Error('ACS method not supported') + else if cwmpRequest.methodResponse? + taskId = cwmpRequest.id - currentRequest.sessionId = cwmpRequest.cookies.ID - taskId = cwmpRequest.cookies.task - - if not taskId - nextTask(currentRequest, cookies) - else db.getTask(taskId, (task) -> if not task nextTask(currentRequest, cookies) - else if cwmpRequest.fault? - util.log("#{currentRequest.deviceId}: Fault response for task #{task._id}") - db.tasksCollection.update({_id : mongodb.ObjectID(String(task._id))}, {$set : {fault : cwmpRequest.fault}}, (err) -> - # Faulty task. No more work to do until task is deleted. - res = tr069.response(currentRequest.sessionId, cwmpResponse, cookies) - writeResponse(currentRequest, res) - ) else - runTask(currentRequest, task, cwmpRequest) + runTask(currentRequest, task, cwmpRequest.methodResponse) ) + else if cwmpRequest.fault? + taskId = cwmpRequest.id + util.log("#{currentRequest.deviceId}: Fault response for task #{taskId}") + db.tasksCollection.update({_id : mongodb.ObjectID(taskId)}, {$set : {fault : cwmpRequest.fault}}, (err) -> + # Faulty task. No more work to do until task is deleted. + res = tr069.response(null, cwmpResponse, cookies) + writeResponse(currentRequest, res) + ) + else + # cpe sent empty response. start sending acs requests + nextTask(currentRequest, cookies) + server.listen config.ACS_PORT, config.ACS_INTERFACE #console.log "Server listening on port #{config.ACS_PORT}" diff --git a/tasks.coffee b/tasks.coffee index e107cf9..a984d10 100644 --- a/tasks.coffee +++ b/tasks.coffee @@ -1,13 +1,13 @@ common = require './common' BATCH_SIZE = 16 -this.init = (task, cwmpRequest, cwmpResponse) -> +this.init = (task, methodResponse, cwmpResponse) -> ret = {} if not task.subtask? task.subtask = {'name' : 'getParameterNames', 'parameterPath' : '', 'nextLevel' : false} if task.subtask.name == 'getParameterNames' - common.extend(ret, this.getParameterNames(task.subtask, cwmpRequest, cwmpResponse)) + common.extend(ret, this.getParameterNames(task.subtask, methodResponse, cwmpResponse)) if ret.parameterNames? # task finished parameterNames = [] for p in ret.parameterNames @@ -16,37 +16,44 @@ this.init = (task, cwmpRequest, cwmpResponse) -> task.subtask = {'name' : 'getParameterValues', 'parameterNames' : parameterNames} if task.subtask.name == 'getParameterValues' - common.extend(ret, this.getParameterValues(task.subtask, cwmpRequest, cwmpResponse)) + common.extend(ret, this.getParameterValues(task.subtask, methodResponse, cwmpResponse)) ret -this.getParameterNames = (task, cwmpRequest, cwmpResponse) -> - if cwmpRequest.getParameterNamesResponse? - return {'parameterNames' : cwmpRequest.getParameterNamesResponse} +this.getParameterNames = (task, methodResponse, cwmpResponse) -> + if methodResponse.type is 'GetParameterNamesResponse' + return {'parameterNames' : methodResponse.parameterList} else - cwmpResponse.getParameterNames = [task.parameterPath, if task.nextLevel? task.nextLevel else false] + cwmpResponse.methodRequest = { + type : 'GetParameterNames', + parameterPath : task.parameterPath, + nextLevel : if task.nextLevel? then task.nextLevel else false + } return -this.getParameterValues = (task, cwmpRequest, cwmpResponse) -> +this.getParameterValues = (task, methodResponse, cwmpResponse) -> if not task.currentIndex? task.currentIndex = 0 - else if cwmpRequest.getParameterValuesResponse? + else if methodResponse.parameterList? task.currentIndex = task.nextIndex task.nextIndex = Math.min(task.currentIndex + BATCH_SIZE, task.parameterNames.length) names = task.parameterNames.slice(task.currentIndex, task.nextIndex) if names.length > 0 - cwmpResponse.getParameterValues = names + cwmpResponse.methodRequest = { + type : 'GetParameterValues', + parameterNames : names + } - if cwmpRequest.getParameterValuesResponse - return {'parameterValues' : cwmpRequest.getParameterValuesResponse} + if methodResponse.type is 'GetParameterValuesResponse' + return {'parameterValues' : methodResponse.parameterList} return -this.setParameterValues = (task, cwmpRequest, cwmpResponse) -> +this.setParameterValues = (task, methodResponse, cwmpResponse) -> if not task.currentIndex? task.currentIndex = 0 - else if cwmpRequest.setParameterValuesResponse? + else if methodResponse.type is 'SetParameterValuesResponse' prevValues = task.parameterValues.slice(task.currentIndex, task.nextIndex) task.currentIndex = task.nextIndex @@ -54,15 +61,20 @@ this.setParameterValues = (task, cwmpRequest, cwmpResponse) -> values = task.parameterValues.slice(task.currentIndex, task.nextIndex) if values.length > 0 - cwmpResponse.setParameterValues = values + cwmpResponse.methodRequest = { + type : 'SetParameterValues', + parameterList : values + } if prevValues? return {'parameterValues' : prevValues} return -this.reboot = (task, cwmpRequest, cwmpResponse) -> - if not cwmpRequest.reboot - cwmpResponse.reboot = 'reboot' +this.reboot = (task, methodResponse, cwmpResponse) -> + if methodResponse.type isnt 'RebootResponse' + cwmpResponse.methodRequest = { + type : 'Reboot' + } -exports.task = (task, cwmpRequest, cwmpResponse) -> - return this[task.name](task, cwmpRequest, cwmpResponse) +exports.task = (task, methodResponse, cwmpResponse) -> + return this[task.name](task, methodResponse, cwmpResponse) diff --git a/tr-069.coffee b/tr-069.coffee index 5e5948c..adbe1f7 100644 --- a/tr-069.coffee +++ b/tr-069.coffee @@ -10,16 +10,13 @@ NAMESPACES = { 'cwmp' : 'urn:dslforum-org:cwmp-1-0', } -trim = (str) -> - str.replace /^\s+|\s+$/g, '' - cookiesToObj = (cookieLine) -> cookies = {} - for c in trim(cookieLine).split ',' + for c in cookieLine.trim().split ',' nv = c.split '=' continue if nv.length < 2 - cookies[trim(nv[0])] = trim(nv[1]) + cookies[nv[0].trim()] = nv[1].trim() return cookies @@ -30,108 +27,105 @@ cookiesToStr = (obj) -> return l# + 'path=/' -sessionId = (xml) -> - e = xml.get('//soap-env:Envelope/soap-env:Header/cwmp:ID', NAMESPACES) - if e? - return trim(e.text()) - return undefined +event = (xml) -> + e.text().trim() for e in xml.find('EventStruct/EventCode', NAMESPACES) -deviceId = (xml) -> - e = xml.get('//soap-env:Envelope/soap-env:Body/cwmp:Inform/DeviceId', NAMESPACES) - if e? - id = {} - for n in e.find('//child::*') - id[n.name()] = trim(n.text()) - return id - return undefined - - -eventCodes = (xml) -> - (trim(e.text()) for e in xml.find('//soap-env:Envelope/soap-env:Body/cwmp:Inform/Event/EventStruct/EventCode', NAMESPACES)) - - -retryCount = (xml) -> - e = xml.get('//soap-env:Envelope/soap-env:Body/cwmp:Inform/RetryCount', NAMESPACES) - if e - return JSON.parse(e.text()) - return undefined - - -informParameterValues = (xml) -> - if not xml.get( '//soap-env:Envelope/soap-env:Body/cwmp:Inform/ParameterList', NAMESPACES) - return undefined - - values = [] - for e in xml.find('//soap-env:Envelope/soap-env:Body/cwmp:Inform/ParameterList/ParameterValueStruct', NAMESPACES) - valueType = trim(e.get('Value').attr('type').value()) - name = trim(e.get('Name').text()) - switch valueType - when 'xsd:boolean' - value = Boolean(JSON.parse(e.get('Value').text())) - when 'xsd:unsignedInt' - value = JSON.parse(e.get('Value').text()) - else - value = trim(e.get('Value').text()) - - values.push([name, value]) - return values - - -getParameterNamesResponse = (xml) -> - if not xml.get('//soap-env:Envelope/soap-env:Body/cwmp:GetParameterNamesResponse', NAMESPACES) - return undefined - - response = [] - for e in xml.find('//soap-env:Envelope/soap-env:Body/cwmp:GetParameterNamesResponse/ParameterList/ParameterInfoStruct', NAMESPACES) - name = trim(e.get('Name').text()) +parameterInfoList = (xml) -> + infoList = [] + for e in xml.find('ParameterInfoStruct', NAMESPACES) + name = e.get('Name').text().trim() writable = Boolean(JSON.parse(e.get('Writable').text())) - response.push([name, writable]) - return response + infoList.push([name, writable]) + return infoList -getParameterValuesResponse = (xml) -> - if not xml.get( '//soap-env:Envelope/soap-env:Body/cwmp:GetParameterValuesResponse', NAMESPACES) - return undefined - - values = [] - for e in xml.find('//soap-env:Envelope/soap-env:Body/cwmp:GetParameterValuesResponse/ParameterList/ParameterValueStruct', NAMESPACES) - valueType = trim(e.get('Value').attr('type').value()) - name = trim(e.get('Name').text()) +parameterValueList = (xml) -> + valueList = [] + for e in xml.find('ParameterValueStruct', NAMESPACES) + valueType = e.get('Value').attr('type').value().trim() + name = e.get('Name').text().trim() + value = e.get('Value').text().trim() switch valueType when 'xsd:boolean' - value = Boolean(JSON.parse(e.get('Value').text())) + value = Boolean(JSON.parse(value)) when 'xsd:unsignedInt' - value = JSON.parse(e.get('Value').text()) - else - value = trim(e.get('Value').text()) - - values.push([name, value]) - return values + value = JSON.parse(value) + valueList.push([name, value]) + return valueList -setParameterValuesResponse = (xml) -> - e = xml.get('//soap-env:Envelope/soap-env:Body/cwmp:SetParameterValuesResponse/Status', NAMESPACES) - if e - return JSON.parse(e.text()) - return undefined +cpeGetParameterNames = (xml, methodRequest) -> + el = xml.node('cwmp:GetParameterNames') + el.node('cwmp:ParameterPath').text(methodRequest.parameterPath) + el.node('cwmp:NextLevel').text(+methodRequest.nextLevel) + + +cpeGetParameterNamesResponse = (xml) -> + {parameterList : parameterInfoList(xml.get('ParameterList'))} + + +cpeGetParameterValues = (xml, methodRequest) -> + el = xml.node('cwmp:GetParameterValues').node('cwmp:ParameterNames') + for p in methodRequest.parameterNames + el.node('xsd:string').text(p) + + +cpeGetParameterValuesResponse = (xml) -> + {parameterList : parameterValueList(xml.get('ParameterList'))} + + +cpeSetParameterValues = (xml, methodRequest) -> + el = xml.node('cwmp:SetParameterValues') + paramList = el.node('ParameterList') + for i in methodRequest.parameterList + pvs = paramList.node('ParameterValueStruct') + pvs.node('Name').text(i[0]) + pvs.node('Value').text(i[1]) + # Huawei CPEs need this element present otherwise won't respond + el.node('ParameterKey').text(if methodRequest.parameterKey? then methodRequest.parameterKey else '') + + +cpeSetParameterValuesResponse = (xml) -> + {status : JSON.parse(xml.get('Status').text())} + + +traverseXml = (xml) -> + obj = {} + for n in xml.childNodes() + if n.type() == 'element' + obj[n.name()] = traverseXml(n) + + if Object.keys(obj).length == 0 + xml.text() + else + obj fault = (xml) -> - f = xml.get('//soap-env:Envelope/soap-env:Body/soap-env:Fault', NAMESPACES) - if f - traverse = (el) -> - children = el.childNodes() - obj = {} - for n in el.childNodes() - if n.type() == 'element' - obj[n.name()] = traverse(n) - if Object.keys(obj).length == 0 - return el.text() - else - return obj + return traverseXml(xml) - return traverse(f) - return undefined + +acsInform = (xml) -> + { + parameterList : parameterValueList(xml.get('ParameterList')), + deviceId : traverseXml(xml.get('DeviceId')), + event : event(xml.get('Event')), + retryCount : JSON.parse(xml.get('RetryCount').text()) + } + + +acsInformResponse = (xml) -> + xml.node('cwmp:InformResponse').node('MaxEnvelopes').text(1) + + +cpeReboot = (xml, methodRequest) -> + el = xml.node('cwmp:Reboot') + # Huawei CPEs need this element present otherwise won't respond + el.node('CommandKey').text(if methodRequest.commandKey then methodRequest.commandKey else '') + + +cpeRebootResponse = (xml) -> + {} exports.request = (httpRequest) -> @@ -147,21 +141,36 @@ exports.request = (httpRequest) -> # some devices send invalid utf8 characters xml = libxmljs.parseXml httpRequest.getBody('binary') - cwmpRequest.sessionId = sessionId xml - cwmpRequest.inform = true if xml.get('//soap-env:Envelope/soap-env:Body/cwmp:Inform', NAMESPACES) - cwmpRequest.informParameterValues = informParameterValues xml - cwmpRequest.reboot = true if xml.get('//soap-env:Envelope/soap-env:Body/cwmp:RebootResponse', NAMESPACES) - cwmpRequest.deviceId = deviceId xml - cwmpRequest.eventCodes = eventCodes xml - cwmpRequest.retryCount = retryCount xml - cwmpRequest.getParameterNamesResponse = getParameterNamesResponse xml - cwmpRequest.getParameterValuesResponse = getParameterValuesResponse xml - cwmpRequest.setParameterValuesResponse = setParameterValuesResponse xml - cwmpRequest.fault = fault xml + cwmpRequest.id = xml.get('//soap-env:Envelope/soap-env:Header/cwmp:ID', NAMESPACES).text() + + methodElement = xml.get('/soap-env:Envelope/soap-env:Body/cwmp:*', NAMESPACES) + if methodElement? + switch methodElement.name() + when 'Inform' + cwmpRequest.methodRequest = acsInform(methodElement) + cwmpRequest.methodRequest.type = 'Inform' + when 'GetParameterNamesResponse' + cwmpRequest.methodResponse = cpeGetParameterNamesResponse(methodElement) + cwmpRequest.methodResponse.type = 'GetParameterNamesResponse' + when 'GetParameterValuesResponse' + cwmpRequest.methodResponse = cpeGetParameterValuesResponse(methodElement) + cwmpRequest.methodResponse.type = 'GetParameterValuesResponse' + when 'SetParameterValuesResponse' + cwmpRequest.methodResponse = cpeSetParameterValuesResponse(methodElement) + cwmpRequest.methodResponse.type = 'SetParameterValuesResponse' + when 'RebootResponse' + cwmpRequest.methodResponse = cpeRebootResponse(methodElement) + cwmpRequest.methodResponse.type = 'RebootResponse' + else + throw Error('8000 Method not supported') + else + faultElement = xml.get('/soap-env:Envelope/soap-env:Body/soap-env:Fault', NAMESPACES) + cwmpRequest.fault = fault(faultElement) + return cwmpRequest -exports.response = (sessionId, cwmpResponse, cookies = null) -> +exports.response = (id, cwmpResponse, cookies = null) -> headers = { 'Content-Type' : 'text/xml; charset="utf-8"', 'Server' : SERVER_NAME, @@ -183,35 +192,29 @@ exports.response = (sessionId, cwmpResponse, cookies = null) -> env.defineNamespace prefix, url header = env.node('soap-env:Header') - header.node('cwmp:ID').attr({'soap-env:mustUnderstand' : 1}).text(sessionId) - #header.node('cwmp:NoMoreRequests').text(0) # depracated + header.node('cwmp:ID').attr({'soap-env:mustUnderstand' : 1}).text(id) body = env.node('soap-env:Body') - if cwmpResponse.inform - body.node('cwmp:InformResponse').node('MaxEnvelopes').text(1) - - if cwmpResponse.getParameterNames? - getParameterNames = body.node('cwmp:GetParameterNames') - getParameterNames.node('cwmp:ParameterPath').text(cwmpResponse.getParameterNames[0]) - getParameterNames.node('cwmp:NextLevel').text(+cwmpResponse.getParameterNames[1]) - - if cwmpResponse.getParameterValues? - parameterNames = body.node('cwmp:GetParameterValues').node('cwmp:ParameterNames') - for p in cwmpResponse.getParameterValues - parameterNames.node('xsd:string').text(p) - - if cwmpResponse.setParameterValues? - setParameterValuesNode = body.node('cwmp:SetParameterValues') - parameterList = setParameterValuesNode.node('ParameterList') - for i in cwmpResponse.setParameterValues - pvs = parameterList.node('ParameterValueStruct') - pvs.node('Name').text(i[0]) - pvs.node('Value').text(i[1]) - setParameterValuesNode.node('ParameterKey') - - if cwmpResponse.reboot? - body.node('cwmp:Reboot').node('CommandKey').text(cwmpResponse.reboot) - + if cwmpResponse.methodResponse? + switch cwmpResponse.methodResponse.type + when 'InformResponse' + acsInformResponse(body) + else + throw Error("Unknown method response type #{cwmpResponse.methodResponse.type}") + else if cwmpResponse.methodRequest? + switch cwmpResponse.methodRequest.type + when 'GetParameterNames' + cpeGetParameterNames(body, cwmpResponse.methodRequest) + when 'GetParameterValues' + cpeGetParameterValues(body, cwmpResponse.methodRequest) + when 'SetParameterValues' + cpeSetParameterValues(body, cwmpResponse.methodRequest) + when 'Reboot' + cpeReboot(body, cwmpResponse.methodRequest) + else + throw Error("Unknown method request #{cwmpResponse.methodRequest.type}") + else + throw Error('Invalid response arguments') data = xml.toString() headers['Content-Length'] = data.length