diff --git a/lib/cwmp.coffee b/lib/cwmp.coffee index 6cda906..0707d4a 100644 --- a/lib/cwmp.coffee +++ b/lib/cwmp.coffee @@ -151,7 +151,12 @@ inform = (currentRequest, cwmpRequest) -> updateAndRespond = () -> updateDevice(currentRequest, actions, (err) -> throw err if err - res = soap.response(cwmpRequest.id, {methodResponse : {type : 'InformResponse'}}, {deviceId : currentRequest.deviceId}) + res = soap.response({ + id : cwmpRequest.id, + methodResponse : {type : 'InformResponse'}, + cwmpVersion : cwmpRequest.cwmpVersion, + cookies : {deviceId : currentRequest.deviceId} + }) writeResponse(currentRequest, res) ) @@ -239,7 +244,7 @@ inform = (currentRequest, cwmpRequest) -> runTask = (currentRequest, task, methodResponse) -> timeDiff = process.hrtime() - tasks.task(task, methodResponse, (err, status, cwmpResponse, deviceUpdates) -> + tasks.task(task, methodResponse, (err, status, methodRequest, deviceUpdates) -> throw err if err # TODO handle error updateDevice(currentRequest, deviceUpdates, (err) -> @@ -256,7 +261,11 @@ runTask = (currentRequest, task, methodResponse) -> f = () -> db.redisClient.setex(String(task._id), config.CACHE_DURATION, JSON.stringify(task), (err) -> throw err if err - res = soap.response(task._id, cwmpResponse) + res = soap.response({ + id : task._id, + methodRequest : methodRequest, + cwmpVersion : currentRequest.cwmpVersion + }) writeResponse(currentRequest, res) ) @@ -304,7 +313,7 @@ assertPresets = (currentRequest) -> presetsHash = res[1] if devicePresetsHash? and devicePresetsHash == presetsHash # no discrepancy, return empty response - res = soap.response(null, {}) + res = soap.response(null) writeResponse(currentRequest, res) else db.getPresetsObjectsAliases((allPresets, allObjects, allAliases) -> @@ -345,7 +354,7 @@ assertPresets = (currentRequest) -> runTask(currentRequest, task, {}) ) else - res = soap.response(null, {}, {}) + res = soap.response(null) writeResponse(currentRequest, res) ) ) @@ -358,7 +367,6 @@ nextTask = (currentRequest) -> cur = db.tasksCollection.find({'device' : currentRequest.deviceId, timestamp : {$lte : now}}).sort(['timestamp']).limit(1) cur.nextObject((err, task) -> throw err if err - cwmpResponse = {} if not task # no more tasks, check presets discrepancy @@ -402,12 +410,12 @@ listener = (httpRequest, httpResponse) -> return body.toString(encoding || 'utf8', 0, body.byteLength) httpRequest.addListener 'end', () -> - cwmpResponse = {} cwmpRequest = soap.request(httpRequest) currentRequest = { httpRequest : httpRequest httpResponse : httpResponse + cwmpVersion : cwmpRequest.cwmpVersion } if cwmpRequest.methodRequest?.deviceId? @@ -427,13 +435,21 @@ listener = (httpRequest, httpResponse) -> else if cwmpRequest.methodRequest.type is 'TransferComplete' # do nothing util.log("#{currentRequest.deviceId}: Transfer complete") - cwmpResponse.methodResponse = {type : 'TransferCompleteResponse'} - res = soap.response(cwmpRequest.id, cwmpResponse, cookies) + res = soap.response({ + id : cwmpRequest.id, + methodResponse : {type : 'TransferCompleteResponse'}, + cwmpVersion : currentRequest.cwmpVersion, + cookies : cookies + }) writeResponse(currentRequest, res) else if cwmpRequest.methodRequest.type is 'RequestDownload' requestDownloadResponse = () -> - cwmpResponse.methodResponse = {type : 'RequestDownloadResponse'} - res = soap.response(cwmpRequest.id, cwmpResponse, cookies) + res = soap.response({ + id : cwmpRequest.id, + methodResponse : {type : 'RequestDownloadResponse'}, + cwmpVersion : currentRequest.cwmpVersion, + cookies : cookies + }) writeResponse(currentRequest, res) fileType = cwmpRequest.methodRequest.fileType util.log("#{currentRequest.deviceId}: RequestDownload (#{fileType})") @@ -491,7 +507,7 @@ listener = (httpRequest, httpResponse) -> taskId = cwmpRequest.id if not taskId # Fault not related to a task. return empty response. - res = soap.response(null, {}, {}) + res = soap.response(null) writeResponse(currentRequest, res) return diff --git a/lib/soap.coffee b/lib/soap.coffee index 2566166..e8d7091 100644 --- a/lib/soap.coffee +++ b/lib/soap.coffee @@ -3,11 +3,41 @@ libxmljs = require 'libxmljs' SERVER_NAME = "GenieACS/#{require('../package.json').version}" NAMESPACES = { - 'soap-enc' : 'http://schemas.xmlsoap.org/soap/encoding/', - 'soap-env' : 'http://schemas.xmlsoap.org/soap/envelope/', - 'xsd' : 'http://www.w3.org/2001/XMLSchema', - 'xsi' : 'http://www.w3.org/2001/XMLSchema-instance', - 'cwmp' : 'urn:dslforum-org:cwmp-1-0', + '1.0' : { + 'soap-enc' : 'http://schemas.xmlsoap.org/soap/encoding/', + 'soap-env' : 'http://schemas.xmlsoap.org/soap/envelope/', + 'xsd' : 'http://www.w3.org/2001/XMLSchema', + 'xsi' : 'http://www.w3.org/2001/XMLSchema-instance', + 'cwmp' : 'urn:dslforum-org:cwmp-1-0' + }, + '1.1' : { + 'soap-enc' : 'http://schemas.xmlsoap.org/soap/encoding/', + 'soap-env' : 'http://schemas.xmlsoap.org/soap/envelope/', + 'xsd' : 'http://www.w3.org/2001/XMLSchema', + 'xsi' : 'http://www.w3.org/2001/XMLSchema-instance', + 'cwmp' : 'urn:dslforum-org:cwmp-1-1' + }, + '1.2' : { + 'soap-enc' : 'http://schemas.xmlsoap.org/soap/encoding/', + 'soap-env' : 'http://schemas.xmlsoap.org/soap/envelope/', + 'xsd' : 'http://www.w3.org/2001/XMLSchema', + 'xsi' : 'http://www.w3.org/2001/XMLSchema-instance', + 'cwmp' : 'urn:dslforum-org:cwmp-1-2' + }, + '1.3' : { + 'soap-enc' : 'http://schemas.xmlsoap.org/soap/encoding/', + 'soap-env' : 'http://schemas.xmlsoap.org/soap/envelope/', + 'xsd' : 'http://www.w3.org/2001/XMLSchema', + 'xsi' : 'http://www.w3.org/2001/XMLSchema-instance', + 'cwmp' : 'urn:dslforum-org:cwmp-1-2' + }, + '1.4' : { + 'soap-enc' : 'http://schemas.xmlsoap.org/soap/encoding/', + 'soap-env' : 'http://schemas.xmlsoap.org/soap/envelope/', + 'xsd' : 'http://www.w3.org/2001/XMLSchema', + 'xsi' : 'http://www.w3.org/2001/XMLSchema-instance', + 'cwmp' : 'urn:dslforum-org:cwmp-1-3' + } } @@ -28,12 +58,12 @@ cookiesToStr = (obj) -> event = (xml) -> - e.text().trim() for e in xml.find('EventStruct/EventCode', NAMESPACES) + e.text().trim() for e in xml.find('EventStruct/EventCode') parameterInfoList = (xml) -> infoList = [] - for e in xml.find('ParameterInfoStruct', NAMESPACES) + for e in xml.find('ParameterInfoStruct') name = e.get('Name').text().trim() writable = Boolean(JSON.parse(e.get('Writable').text())) infoList.push([name, writable]) @@ -42,7 +72,7 @@ parameterInfoList = (xml) -> parameterValueList = (xml) -> valueList = [] - for e in xml.find('ParameterValueStruct', NAMESPACES) + for e in xml.find('ParameterValueStruct') valueType = e.get('Value').attr('type').value().trim() name = e.get('Name').text().trim() value = e.get('Value').text().trim() @@ -211,6 +241,7 @@ acsRequestDownloadResponse = (xml, methodRequest) -> exports.request = (httpRequest) -> cwmpRequest = {cookies: {}} cwmpRequest.cookies = cookiesToObj(httpRequest.headers.cookie) if httpRequest.headers.cookie + cwmpRequest.cwmpVersion = cwmpRequest.cookies.cwmpVersion data = httpRequest.getBody() @@ -221,12 +252,31 @@ exports.request = (httpRequest) -> # some devices send invalid utf8 characters xml = libxmljs.parseXml httpRequest.getBody('binary') - try - cwmpRequest.id = xml.get('//soap-env:Envelope/soap-env:Header/cwmp:ID', NAMESPACES).text() - catch err - cwmpRequest.id = null + if not cwmpRequest.cwmpVersion? + # cwmpVersion doesn't exist in cookies, 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' + cwmpRequest.cwmpVersion = '1.0' + when 'urn:dslforum-org:cwmp-1-1' + cwmpRequest.cwmpVersion = '1.1' + when 'urn:dslforum-org:cwmp-1-2' + cwmpRequest.sessionTimeout = try xml.get('/soap-env:Envelope/soap-env:Header/cwmp:sessionTimeout', NAMESPACES['1.2']).text() catch then null + if cwmpRequest.sessionTimeout? + cwmpRequest.cwmpVersion = '1.3' + else + cwmpRequest.cwmpVersion = '1.2' + when 'urn:dslforum-org:cwmp-1-3' + cwmpRequest.cwmpVersion = '1.4' + else + throw new Error('Unrecognized CWMP version') + + cwmpRequest.sessionTimeout ?= try xml.get('/soap-env:Envelope/soap-env:Header/cwmp:sessionTimeout', NAMESPACES[cwmpRequest.cwmpVersion]).text() catch then null + else + methodElement = xml.get('/soap-env:Envelope/soap-env:Body/cwmp:*', NAMESPACES[cwmpRequest.cwmpVersion]) + + cwmpRequest.id = try xml.get('/soap-env:Envelope/soap-env:Header/cwmp:ID', NAMESPACES[cwmpRequest.cwmpVersion]).text() catch then null - methodElement = xml.get('/soap-env:Envelope/soap-env:Body/cwmp:*', NAMESPACES) if methodElement? switch methodElement.name() when 'Inform' @@ -265,40 +315,36 @@ exports.request = (httpRequest) -> else throw Error('8000 Method not supported ' + methodElement.name()) else - faultElement = xml.get('/soap-env:Envelope/soap-env:Body/soap-env:Fault', NAMESPACES) + faultElement = xml.get('/soap-env:Envelope/soap-env:Body/soap-env:Fault', NAMESPACES[cwmpRequest.cwmpVersion]) cwmpRequest.fault = fault(faultElement) return cwmpRequest -exports.response = (id, cwmpResponse, cookies = null) -> +createSoapEnv = (cwmpVersion) -> + xml = libxmljs.Document() + env = xml.node('soap-env:Envelope') + for prefix, url of NAMESPACES[cwmpVersion] + env.defineNamespace prefix, url + return env + + +exports.response = (cwmpResponse) -> headers = { 'Content-Type' : 'text/xml; charset="utf-8"', 'Server' : SERVER_NAME, 'SOAPServer' : SERVER_NAME, } - if cookies? and Object.keys(cookies).length > 0 - headers['Set-Cookie'] = cookiesToStr(cookies) - - if not cwmpResponse? or Object.keys(cwmpResponse).length == 0 - #console.log '>>> EMPTY RESPONSE' - # send empty response - headers['Content-Length'] = 0 - return {code: 204, headers: headers, data: ''} - - xml = libxmljs.Document() - env = xml.node('soap-env:Envelope') - for prefix, url of NAMESPACES - env.defineNamespace prefix, url - - header = env.node('soap-env:Header') - header.node('cwmp:ID').attr({'soap-env:mustUnderstand' : 1}).text(id) - body = env.node('soap-env:Body') - - if cwmpResponse.methodResponse? + if cwmpResponse?.methodResponse? + env = createSoapEnv(cwmpResponse.cwmpVersion) + header = env.node('soap-env:Header') + header.node('cwmp:ID').attr({'soap-env:mustUnderstand' : 1}).text(cwmpResponse.id) + body = env.node('soap-env:Body') switch cwmpResponse.methodResponse.type when 'InformResponse' + cwmpResponse.cookies ?= {} + cwmpResponse.cookies.cwmpVersion = cwmpResponse.cwmpVersion acsInformResponse(body) when 'TransferCompleteResponse' acsTransferCompleteResponse(body) @@ -306,7 +352,11 @@ exports.response = (id, cwmpResponse, cookies = null) -> acsRequestDownloadResponse(body) else throw Error("Unknown method response type #{cwmpResponse.methodResponse.type}") - else if cwmpResponse.methodRequest? + else if cwmpResponse?.methodRequest? + env = createSoapEnv(cwmpResponse.cwmpVersion) + header = env.node('soap-env:Header') + header.node('cwmp:ID').attr({'soap-env:mustUnderstand' : 1}).text(cwmpResponse.id) + body = env.node('soap-env:Body') switch cwmpResponse.methodRequest.type when 'GetParameterNames' cpeGetParameterNames(body, cwmpResponse.methodRequest) @@ -326,10 +376,14 @@ exports.response = (id, cwmpResponse, cookies = null) -> cpeDownload(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 + if cwmpResponse?.cookies? and Object.keys(cwmpResponse.cookies).length > 0 + headers['Set-Cookie'] = cookiesToStr(cwmpResponse.cookies) - return {code: 200, headers: headers, data: data} + if env? + data = env.doc().toString() + headers['Content-Length'] = data.length + return {code: 200, headers: headers, data: data} + else + headers['Content-Length'] = 0 + return {code: 204, headers: headers, data: ''} diff --git a/lib/tasks.coffee b/lib/tasks.coffee index 40d53d9..9aef892 100644 --- a/lib/tasks.coffee +++ b/lib/tasks.coffee @@ -25,19 +25,19 @@ this.refreshObject = (task, methodResponse, callback) -> return if task.session.subtask.name is 'getParameterNames' - this.getParameterNames(task.session.subtask, methodResponse, (err, status, cwmpResponse, deviceUpdates) => + this.getParameterNames(task.session.subtask, methodResponse, (err, status, methodRequest, deviceUpdates) => if status & STATUS_COMPLETED parameterNames = [] for p in deviceUpdates.parameterNames if not common.endsWith(p[0], '.') parameterNames.push(p[0]) task.session.subtask = {name : 'getParameterValues', parameterNames : parameterNames} - this.getParameterValues(task.session.subtask, {}, (err, status, cwmpResponse) -> + this.getParameterValues(task.session.subtask, {}, (err, status, methodRequest) -> # ignore deviceUpdates returned by firt call to getParameterValues - callback(err, status, cwmpResponse, deviceUpdates) + callback(err, status, methodRequest, deviceUpdates) ) else if status & STATUS_OK - callback(err, STATUS_OK, cwmpResponse, deviceUpdates) + callback(err, STATUS_OK, methodRequest, deviceUpdates) else throw Error('Unexpected subtask status') ) @@ -46,8 +46,8 @@ this.refreshObject = (task, methodResponse, callback) -> task.fault = methodResponse return callback(null, STATUS_FAULT) - this.getParameterValues(task.session.subtask, methodResponse, (err, status, cwmpResponse, deviceUpdates) -> - callback(err, status, cwmpResponse, deviceUpdates) + this.getParameterValues(task.session.subtask, methodResponse, (err, status, methodRequest, deviceUpdates) -> + callback(err, status, methodRequest, deviceUpdates) ) else throw Error('Unexpected subtask name') @@ -134,7 +134,7 @@ this.getParameterNames = (task, methodResponse, callback) -> parameterPath : task.parameterPath, nextLevel : if task.nextLevel? then task.nextLevel else false } - callback(null, STATUS_OK, {methodRequest : methodRequest}) + callback(null, STATUS_OK, methodRequest) this.getParameterValues = (task, methodResponse, callback) -> @@ -163,7 +163,7 @@ this.getParameterValues = (task, methodResponse, callback) -> type : 'GetParameterValues', parameterNames : names } - callback(null, STATUS_OK, {methodRequest : methodRequest}, deviceUpdates) + callback(null, STATUS_OK, methodRequest, deviceUpdates) this.setParameterValues = (task, methodResponse, callback) -> @@ -189,7 +189,7 @@ this.setParameterValues = (task, methodResponse, callback) -> if values.length == 0 callback(null, STATUS_COMPLETED, null, deviceUpdates) else - callback(null, STATUS_OK, {methodRequest : {type : 'SetParameterValues', parameterList : values}}, deviceUpdates) + callback(null, STATUS_OK, {type : 'SetParameterValues', parameterList : values}, deviceUpdates) this.addObject = (task, methodResponse, callback) -> @@ -211,13 +211,13 @@ this.addObject = (task, methodResponse, callback) -> task.session.parameterNames = [] allDeviceUpdates.instanceName = [["#{task.objectName}.#{task.session.instanceNumber}", task.instanceName]] if task.instanceName? else - callback(null, STATUS_OK, {methodRequest : {type : 'AddObject', objectName : "#{task.objectName}."}}) + callback(null, STATUS_OK, {type : 'AddObject', objectName : "#{task.objectName}."}) return subtask = () => switch task.session.subtask.name when 'getParameterNames' - this.getParameterNames(task.session.subtask, methodResponse, (err, status, cwmpResponse, deviceUpdates) => + this.getParameterNames(task.session.subtask, methodResponse, (err, status, methodRequest, deviceUpdates) => common.extend(allDeviceUpdates, deviceUpdates) if deviceUpdates and deviceUpdates.parameterNames for p in deviceUpdates.parameterNames @@ -228,12 +228,12 @@ this.addObject = (task, methodResponse, callback) -> subtask() else if status & STATUS_OK # Use STATUS_SAVE to avoid adding duplicate object in case of error - callback(err, STATUS_OK | status_save, cwmpResponse, allDeviceUpdates) + callback(err, STATUS_OK | status_save, methodRequest, allDeviceUpdates) else throw Error('Unexpected subtask status') ) when 'getParameterValues' - this.getParameterValues(task.session.subtask, methodResponse, (err, status, cwmpResponse, deviceUpdates) => + this.getParameterValues(task.session.subtask, methodResponse, (err, status, methodRequest, deviceUpdates) => common.extend(allDeviceUpdates, deviceUpdates) # if values are given, compare with default values if task.parameterValues? @@ -254,12 +254,12 @@ this.addObject = (task, methodResponse, callback) -> task.session.subtask = {name : 'setParameterValues', parameterValues : task.session.appliedParameterValues} subtask() else - callback(err, status, cwmpResponse, allDeviceUpdates) + callback(err, status, methodRequest, allDeviceUpdates) ) when 'setParameterValues' - this.setParameterValues(task.session.subtask, methodResponse, (err, status, cwmpResponse, deviceUpdates) => + this.setParameterValues(task.session.subtask, methodResponse, (err, status, methodRequest, deviceUpdates) => common.extend(allDeviceUpdates, deviceUpdates) - callback(err, status, cwmpResponse, allDeviceUpdates) + callback(err, status, methodRequest, allDeviceUpdates) ) subtask() @@ -277,7 +277,7 @@ this.deleteObject = (task, methodResponse, callback) -> type : 'DeleteObject', objectName : "#{task.objectName}." } - callback(null, STATUS_OK, {methodRequest : methodRequest}) + callback(null, STATUS_OK, methodRequest) this.reboot = (task, methodResponse, callback) -> @@ -287,7 +287,7 @@ this.reboot = (task, methodResponse, callback) -> return if methodResponse.type isnt 'RebootResponse' - callback(null, STATUS_OK, {methodRequest : {type : 'Reboot'}}) + callback(null, STATUS_OK, {type : 'Reboot'}) else callback(null, STATUS_COMPLETED) @@ -299,7 +299,7 @@ this.factoryReset = (task, methodResponse, callback) -> return if methodResponse.type isnt 'FactoryResetResponse' - callback(null, STATUS_OK, {methodRequest : {type : 'FactoryReset'}}) + callback(null, STATUS_OK, {type : 'FactoryReset'}) else callback(null, STATUS_COMPLETED) @@ -327,7 +327,7 @@ this.download = (task, methodResponse, callback) -> successUrl : task.successUrl, failureUrl : task.failureUrl } - callback(null, STATUS_OK, {methodRequest : methodRequest}) + callback(null, STATUS_OK, methodRequest) ) else callback(null, STATUS_COMPLETED)