Unify currentRequest and sessionData structures

This commit is contained in:
Zaid Abdulla committed 2017-02-25 16:24:53 -08:00
1 parent bdcb856655
commit 59d412155b
6 files changed
+1285 -1189

No files matched your search

+259 -261
View File
@@ -63,16 +63,16 @@ throwError = (err, httpResponse) ->
throw err
writeResponse = (currentRequest, res) ->
if config.get('DEBUG', currentRequest.sessionData.deviceId)
writeResponse = (sessionContext, res) ->
if config.get('DEBUG', sessionContext.deviceId)
dump = "# RESPONSE #{new Date(Date.now())}\n" + JSON.stringify(res.headers) + "\n#{res.data}\n\n"
require('fs').appendFile("./debug/#{currentRequest.sessionData.deviceId}.dump", dump, (err) ->
require('fs').appendFile("./debug/#{sessionContext.deviceId}.dump", dump, (err) ->
throwError(err) if err
)
# respond using the same content-encoding as the request
if currentRequest.httpRequest.headers['content-encoding']? and res.data.length > 0
switch currentRequest.httpRequest.headers['content-encoding']
if sessionContext.httpRequest.headers['content-encoding']? and res.data.length > 0
switch sessionContext.httpRequest.headers['content-encoding']
when 'gzip'
res.headers['Content-Encoding'] = 'gzip'
compress = zlib.gzip
@@ -82,44 +82,50 @@ writeResponse = (currentRequest, res) ->
if compress?
compress(res.data, (err, data) ->
return throwError(err, currentRequest.httpResponse) if err
return throwError(err, sessionContext.httpResponse) if err
res.headers['Content-Length'] = data.length
currentRequest.httpResponse.writeHead(res.code, res.headers)
currentRequest.httpResponse.end(data)
sessionContext.httpResponse.writeHead(res.code, res.headers)
sessionContext.httpResponse.end(data)
)
else
res.headers['Content-Length'] = res.data.length
currentRequest.httpResponse.writeHead(res.code, res.headers)
currentRequest.httpResponse.end(res.data)
sessionContext.httpResponse.writeHead(res.code, res.headers)
sessionContext.httpResponse.end(res.data)
delete sessionContext.httpRequest
delete sessionContext.httpResponse
recordFault = (currentRequest, fault, provisions, channels) ->
sessionData = currentRequest.sessionData
faults = sessionData.faults
recordFault = (sessionContext, fault, provisions, channels) ->
if not provisions
provisions = sessionContext.provisions
channels = sessionContext.channels
faults = sessionContext.faults
for channel of channels
provs = sessionData.faults[channel]?.provisions or []
provs = sessionContext.faults[channel]?.provisions or []
faults[channel] = Object.assign({provisions: provs}, fault)
if sessionData.retries[channel]?
++ sessionData.retries[channel]
if sessionContext.retries[channel]?
++ sessionContext.retries[channel]
else
sessionData.retries[channel] = 0
sessionContext.retries[channel] = 0
if Object.keys(channels).length != 1
faults[channel].retryNow = true
if channels[channel] == 0
faults[channel].precondition = true
sessionData.faultsTouched ?= {}
sessionData.faultsTouched[channel] = true
sessionContext.faultsTouched ?= {}
sessionContext.faultsTouched[channel] = true
logger.accessError({
currentRequest: currentRequest
sessionContext: sessionContext
message: 'Channel has faulted'
fault: fault
channel: channel
retries: sessionData.retries[channel]
retries: sessionContext.retries[channel]
})
for provision, i in provisions
@@ -132,51 +138,51 @@ recordFault = (currentRequest, fault, provisions, channels) ->
faults[channel].provisions = []
appendProvisions(faults[channel].provisions, provs)
session.clearProvisions(sessionData)
session.clearProvisions(sessionContext)
return
inform = (currentRequest, cwmpRequest) ->
session.inform(currentRequest.sessionData, cwmpRequest.methodRequest, (err, rpcResponse) ->
return throwError(err, currentRequest.httpResponse) if err
inform = (sessionContext, rpc) ->
session.inform(sessionContext, rpc.cpeRequest, (err, acsResponse) ->
return throwError(err, sessionContext.httpResponse) if err
res = soap.response({
id : cwmpRequest.id,
methodResponse : rpcResponse,
cwmpVersion : currentRequest.sessionData.cwmpVersion
id: rpc.id,
acsResponse: acsResponse,
cwmpVersion : sessionContext.cwmpVersion
})
if !!cookiesPath = config.get('COOKIES_PATH', currentRequest.sessionData.deviceId)
res.headers['Set-Cookie'] = "session=#{currentRequest.sessionData.sessionId}; Path=#{cookiesPath}"
if !!cookiesPath = config.get('COOKIES_PATH', sessionContext.deviceId)
res.headers['Set-Cookie'] = "session=#{sessionContext.sessionId}; Path=#{cookiesPath}"
else
res.headers['Set-Cookie'] = "session=#{currentRequest.sessionData.sessionId}"
res.headers['Set-Cookie'] = "session=#{sessionContext.sessionId}"
writeResponse(currentRequest, res)
writeResponse(sessionContext, res)
)
transferComplete = (currentRequest, cwmpRequest) ->
session.transferComplete(currentRequest.sessionData, cwmpRequest.methodRequest, (err, rpcResponse, operation, fault) ->
return throwError(err, currentRequest.httpResponse) if err
transferComplete = (sessionContext, rpc) ->
session.transferComplete(sessionContext, rpc.cpeRequest, (err, acsResponse, operation, fault) ->
return throwError(err, sessionContext.httpResponse) if err
if not operation?
logger.accessWarn({
currentRequest: currentRequest
sessionContext: sessionContext
message: 'Unrecognized command key'
cpeRequest: cwmpRequest.methodRequest
rpc: rpc
})
if fault
for k, v of operation.retries
currentRequest.sessionData.retries[k] = v
sessionContext.retries[k] = v
recordFault(currentRequest, fault, operation.provisions, operation.channels)
recordFault(sessionContext, fault)
res = soap.response({
id : cwmpRequest.id,
methodResponse : rpcResponse,
cwmpVersion : currentRequest.sessionData.cwmpVersion
id : rpc.id,
acsResponse : acsResponse,
cwmpVersion : sessionContext.cwmpVersion
})
writeResponse(currentRequest, res)
writeResponse(sessionContext, res)
)
@@ -227,38 +233,34 @@ appendProvisions = (original, toAppend) ->
return modified
applyPresets = (currentRequest) ->
sessionData = currentRequest.sessionData
applyPresets = (sessionContext) ->
cache.getPresets((err, presetsHash, presets) ->
return throwError(err, currentRequest.httpResponse) if err
return throwError(err, sessionContext.httpResponse) if err
# Filter presets based on existing faults
blackList = {}
whiteList = null
whiteListProvisions = null
RETRY_DELAY = config.get('RETRY_DELAY', sessionData.deviceId)
RETRY_DELAY = config.get('RETRY_DELAY', sessionContext.deviceId)
if sessionData.faults?
for channel, fault of sessionData.faults
if sessionContext.faults?
for channel, fault of sessionContext.faults
if fault.retryNow
retryTimestamp = 0
else
retryTimestamp = fault.timestamp + (RETRY_DELAY * Math.pow(2, sessionData.retries[channel])) * 1000
retryTimestamp = fault.timestamp + (RETRY_DELAY * Math.pow(2, sessionContext.retries[channel])) * 1000
if retryTimestamp <= sessionData.timestamp
if retryTimestamp <= sessionContext.timestamp
whiteList = channel
whiteListProvisions = fault.provisions
break
blackList[channel] = if fault.precondition then 1 else 2
sessionData.presetsHash = presetsHash
deviceEvents = {}
for p in sessionData.deviceData.paths.find(['Events', '*'], false, true)
if sessionData.timestamp <= sessionData.deviceData.attributes.get(p)?.value?[1][0]
for p in sessionContext.deviceData.paths.find(['Events', '*'], false, true)
if sessionContext.timestamp <= sessionContext.deviceData.attributes.get(p)?.value?[1][0]
deviceEvents[p[1]] = true
parameters = {}
@@ -277,11 +279,11 @@ applyPresets = (currentRequest) ->
break
continue if (not eventsMatch) or (preset.schedule and not
(preset.schedule.schedule and testSchedule(preset.schedule, sessionData.timestamp)[0]))
(preset.schedule.schedule and testSchedule(preset.schedule, sessionContext.timestamp)[0]))
filteredPresets.push(preset)
for k of preset.precondition
sessionData.channels[preset.channel] = 0
sessionContext.channels[preset.channel] = 0
p = k.split(/([^a-zA-Z0-9\-\_\.].*)/, 1)[0]
parameters[p] = common.parsePath(p)
@@ -289,28 +291,28 @@ applyPresets = (currentRequest) ->
for k, v of parameters
declarations.push([v, 1, {value: 1}])
session.rpcRequest(sessionData, declarations, (err, fault, id, rpcRequest) ->
return throwError(err, currentRequest.httpResponse) if err
session.rpcRequest(sessionContext, declarations, (err, fault, id, acsRequest) ->
return throwError(err, sessionContext.httpResponse) if err
if fault
recordFault(currentRequest, fault, sessionData.provisions, sessionData.channels)
session.clearProvisions(sessionData)
return applyPresets(currentRequest)
recordFault(sessionContext, fault)
session.clearProvisions(sessionContext)
return applyPresets(sessionContext)
if rpcRequest?
return sendRpcRequest(currentRequest, id, rpcRequest)
if acsRequest
return sendAcsRequest(sessionContext, id, acsRequest)
session.clearProvisions(sessionData)
session.clearProvisions(sessionContext)
for k, v of parameters
unpacked = device.unpack(sessionData.deviceData, v)
if unpacked[0] and (vv = sessionData.deviceData.attributes.get(unpacked[0]).value?[1])?
unpacked = device.unpack(sessionContext.deviceData, v)
if unpacked[0] and (vv = sessionContext.deviceData.attributes.get(unpacked[0]).value?[1])?
parameters[k] = vv[0]
else
delete parameters[k]
if whiteList?
session.addProvisions(sessionData, whiteList, whiteListProvisions)
session.addProvisions(sessionContext, whiteList, whiteListProvisions)
appendProvisionsToFaults = {}
for p in filteredPresets
@@ -319,221 +321,222 @@ applyPresets = (currentRequest) ->
appendProvisionsToFaults[p.channel] =
(appendProvisionsToFaults[p.channel] or []).concat(p.provisions)
else
session.addProvisions(sessionData, p.channel, p.provisions)
session.addProvisions(sessionContext, p.channel, p.provisions)
for channel, provisions of appendProvisionsToFaults
if appendProvisions(sessionData.faults[channel].provisions, provisions)
sessionData.faultsTouched ?= {}
sessionData.faultsTouched[channel] = true
if appendProvisions(sessionContext.faults[channel].provisions, provisions)
sessionContext.faultsTouched ?= {}
sessionContext.faultsTouched[channel] = true
sessionData.presetCycles = (sessionData.presetCycles or 0) + 1
sessionContext.presetCycles = (sessionContext.presetCycles or 0) + 1
if sessionData.presetCycles > MAX_CYCLES
if sessionContext.presetCycles > MAX_CYCLES
fault = {
code: 'endless_cycle'
message: 'The provision seems to be repeating indefinitely'
timestamp: sessionData.timestamp
timestamp: sessionContext.timestamp
}
recordFault(currentRequest, fault, sessionData.provisions, sessionData.channels)
session.clearProvisions(sessionData)
return sendRpcRequest(currentRequest)
recordFault(sessionContext, fault)
session.clearProvisions(sessionContext)
return sendAcsRequest(sessionContext)
session.rpcRequest(sessionData, null, (err, fault, id, rpcRequest) ->
return throwError(err, currentRequest.httpResponse) if err
session.rpcRequest(sessionContext, null, (err, fault, id, acsRequest) ->
return throwError(err, sessionContext.httpResponse) if err
if fault
recordFault(currentRequest, fault, sessionData.provisions, sessionData.channels)
session.clearProvisions(sessionData)
return applyPresets(currentRequest)
recordFault(sessionContext, fault)
session.clearProvisions(sessionContext)
return applyPresets(sessionContext)
if not rpcRequest?
for channel, flags of sessionData.channels
if channel of sessionData.faults
delete sessionData.faults[channel]
sessionData.faultsTouched ?= {}
sessionData.faultsTouched[channel] = true
if not acsRequest
for channel, flags of sessionContext.channels
if channel of sessionContext.faults
delete sessionContext.faults[channel]
sessionContext.faultsTouched ?= {}
sessionContext.faultsTouched[channel] = true
if whiteList?
return applyPresets(currentRequest)
return applyPresets(sessionContext)
sendRpcRequest(currentRequest, id, rpcRequest)
sendAcsRequest(sessionContext, id, acsRequest)
)
)
)
nextRpc = (currentRequest) ->
session.rpcRequest(currentRequest.sessionData, null, (err, fault, id, rpcRequest) ->
return throwError(err, currentRequest.httpResponse) if err
nextRpc = (sessionContext) ->
session.rpcRequest(sessionContext, null, (err, fault, id, acsRequest) ->
return throwError(err, sessionContext.httpResponse) if err
if fault
recordFault(currentRequest, fault, currentRequest.sessionData.provisions, currentRequest.sessionData.channels)
session.clearProvisions(currentRequest.sessionData)
return nextRpc(currentRequest)
recordFault(sessionContext, fault)
session.clearProvisions(sessionContext)
return nextRpc(sessionContext)
if rpcRequest?
return sendRpcRequest(currentRequest, id, rpcRequest)
if acsRequest
return sendAcsRequest(sessionContext, id, acsRequest)
for channel, flags of currentRequest.sessionData.channels
if flags and channel of currentRequest.sessionData.faults
delete currentRequest.sessionData.faults[channel]
currentRequest.sessionData.faultsTouched ?= {}
currentRequest.sessionData.faultsTouched[channel] = true
for channel, flags of sessionContext.channels
if flags and channel of sessionContext.faults
delete sessionContext.faults[channel]
sessionContext.faultsTouched ?= {}
sessionContext.faultsTouched[channel] = true
if channel.startsWith('task_')
taskId = channel.slice(5)
currentRequest.sessionData.doneTasks ?= []
currentRequest.sessionData.doneTasks.push(taskId)
for t, j in currentRequest.sessionData.tasks
sessionContext.doneTasks ?= []
sessionContext.doneTasks.push(taskId)
for t, j in sessionContext.tasks
if t._id == taskId
currentRequest.sessionData.tasks.splice(j, 1)
sessionContext.tasks.splice(j, 1)
break
session.clearProvisions(currentRequest.sessionData)
session.clearProvisions(sessionContext)
for task in currentRequest.sessionData.tasks
for task in sessionContext.tasks
channel = "task_#{task._id}"
# Delete if expired
if task.expiry <= currentRequest.sessionData.timestamp
if task.expiry <= sessionContext.timestamp
logger.accessInfo({
currentRequest: currentRequest
sessionContext: sessionContext
message: 'Task expired'
task: task
})
currentRequest.sessionData.doneTasks ?= []
currentRequest.sessionData.doneTasks.push(String(task._id))
if channel of currentRequest.sessionData.faults
delete currentRequest.sessionData.faults[channel]
currentRequest.sessionData.faultsTouched ?= {}
currentRequest.sessionData.faultsTouched[channel] = true
sessionContext.doneTasks ?= []
sessionContext.doneTasks.push(String(task._id))
if channel of sessionContext.faults
delete sessionContext.faults[channel]
sessionContext.faultsTouched ?= {}
sessionContext.faultsTouched[channel] = true
continue
if currentRequest.sessionData.faults[channel]
if sessionContext.faults[channel]
continue
switch task.name
when 'getParameterValues'
# Set channel in case params array is empty
currentRequest.sessionData.channels["task_#{task._id}"] = 0
sessionContext.channels["task_#{task._id}"] = 0
for p in task.parameterNames
session.addProvisions(currentRequest.sessionData, "task_#{task._id}",
session.addProvisions(sessionContext, "task_#{task._id}",
[['refresh', p]])
when 'setParameterValues'
# Set channel in case params array is empty
currentRequest.sessionData.channels["task_#{task._id}"] = 0
sessionContext.channels["task_#{task._id}"] = 0
for p in task.parameterValues
session.addProvisions(currentRequest.sessionData, "task_#{task._id}",
session.addProvisions(sessionContext, "task_#{task._id}",
[['value', p[0], p[1]]])
when 'refreshObject'
session.addProvisions(currentRequest.sessionData, "task_#{task._id}",
session.addProvisions(sessionContext, "task_#{task._id}",
[['refresh', task.objectName]])
when 'reboot'
session.addProvisions(currentRequest.sessionData, "task_#{task._id}",
session.addProvisions(sessionContext, "task_#{task._id}",
[['reboot']])
when 'factoryReset'
session.addProvisions(currentRequest.sessionData, "task_#{task._id}",
session.addProvisions(sessionContext, "task_#{task._id}",
[['reset']])
when 'download'
session.addProvisions(currentRequest.sessionData, "task_#{task._id}",
session.addProvisions(sessionContext, "task_#{task._id}",
[['download', task.fileType, task.fileName, task.targetFileName]])
when 'addObject'
alias = ("#{p[0]}:#{JSON.stringify(p[1])}" for p in task.parameterValues or []).join(',')
session.addProvisions(currentRequest.sessionData, "task_#{task._id}",
session.addProvisions(sessionContext, "task_#{task._id}",
[['instances', "#{task.objectName}.[#{alias}]", "+1"]]);
when 'deleteObject'
session.addProvisions(currentRequest.sessionData, "task_#{task._id}",
session.addProvisions(sessionContext, "task_#{task._id}",
[['instances', task.objectName, 0]]);
else
return throwError(new Error('Task name not recognized'), currentRequest.httpResponse) if err
return throwError(new Error('Task name not recognized'), sessionContext.httpResponse) if err
return nextRpc(currentRequest)
return nextRpc(sessionContext)
return applyPresets(currentRequest)
return applyPresets(sessionContext)
)
sendRpcRequest = (currentRequest, id, rpcRequest) ->
if not rpcRequest?
session.end(currentRequest.sessionData, (err, isNew) ->
return throwError(err, currentRequest.httpResponse) if err
sendAcsRequest = (sessionContext, id, acsRequest) ->
if not acsRequest?
session.end(sessionContext, (err, isNew) ->
return throwError(err, sessionContext.httpResponse) if err
delete currentSessions.get(currentRequest.httpRequest.connection)[currentRequest.sessionData.sessionId]
delete currentSessions.get(sessionContext.httpRequest.connection)[sessionContext.sessionId]
if isNew
logger.accessInfo({
currentRequest: currentRequest
sessionContext: sessionContext
message: 'New device registered'
})
db.clearTasks(currentRequest.sessionData.deviceId, currentRequest.sessionData.doneTasks, (err) ->
return throwError(err, currentRequest.httpResponse) if err
db.clearTasks(sessionContext.deviceId, sessionContext.doneTasks, (err) ->
return throwError(err, sessionContext.httpResponse) if err
counter = 3
for k of currentRequest.sessionData.faultsTouched
for k of sessionContext.faultsTouched
counter += 2
if currentRequest.sessionData.faults[k]
currentRequest.sessionData.faults[k].retries = currentRequest.sessionData.retries[k]
db.saveFault(currentRequest.sessionData.deviceId, k, currentRequest.sessionData.faults[k], (err) ->
if sessionContext.faults[k]
sessionContext.faults[k].retries = sessionContext.retries[k]
db.saveFault(sessionContext.deviceId, k, sessionContext.faults[k], (err) ->
if err
throwError(err, currentRequest.httpResponse) if counter & 1
throwError(err, sessionContext.httpResponse) if counter & 1
return counter = 0
if (counter -= 2) == 1
return writeResponse(currentRequest, soap.response(null))
return writeResponse(sessionContext, soap.response(null))
)
else
db.deleteFault(currentRequest.sessionData.deviceId, k, (err) ->
db.deleteFault(sessionContext.deviceId, k, (err) ->
if err
throwError(err, currentRequest.httpResponse) if counter & 1
throwError(err, sessionContext.httpResponse) if counter & 1
return counter = 0
if (counter -= 2) == 1
return writeResponse(currentRequest, soap.response(null))
return writeResponse(sessionContext, soap.response(null))
)
if (counter -= 2) == 1
return writeResponse(currentRequest, soap.response(null))
return writeResponse(sessionContext, soap.response(null))
)
)
return
if rpcRequest.type is 'Download'
if not rpcRequest.url?
if acsRequest.name is 'Download'
if not acsRequest.url?
FS_PORT = config.get('FS_PORT')
FS_IP = config.get('FS_IP')
FS_SSL = config.get('FS_SSL')
rpcRequest.url = if FS_SSL then 'https://' else 'http://'
rpcRequest.url += FS_IP
rpcRequest.url += ":#{FS_PORT}" if FS_PORT != 80
rpcRequest.url += "/#{encodeURIComponent(rpcRequest.fileName)}"
acsRequest.url = if FS_SSL then 'https://' else 'http://'
acsRequest.url += FS_IP
acsRequest.url += ":#{FS_PORT}" if FS_PORT != 80
acsRequest.url += "/#{encodeURIComponent(acsRequest.fileName)}"
if not rpcRequest.fileSize?
if not acsRequest.fileSize?
return cache.getFiles((err, hash, files) ->
return throwError(err, currentRequest.httpResponse) if err
return throwError(err, sessionContext.httpResponse) if err
if rpcRequest.fileName of files
rpcRequest.fileSize = files[rpcRequest.fileName].length
if acsRequest.fileName of files
acsRequest.fileSize = files[acsRequest.fileName].length
else
rpcRequest.fileSize = 0
acsRequest.fileSize = 0
return sendRpcRequest(currentRequest, id, rpcRequest)
return sendAcsRequest(sessionContext, id, acsRequest)
)
logger.accessInfo({
currentRequest: currentRequest
message: 'ACS request'
acsRequest: rpcRequest
acsRequestId: id
})
res = soap.response({
rpc = {
id : id,
methodRequest : rpcRequest,
cwmpVersion : currentRequest.sessionData.cwmpVersion
acsRequest : acsRequest,
cwmpVersion : sessionContext.cwmpVersion
}
logger.accessInfo({
sessionContext: sessionContext
message: 'ACS request'
rpc: rpc
})
writeResponse(currentRequest, res)
res = soap.response(rpc)
writeResponse(sessionContext, res)
getSession = (httpRequest, callback) ->
@@ -544,18 +547,18 @@ getSession = (httpRequest, callback) ->
return callback() if not sessionId?
sessionData = currentSessions.get(httpRequest.connection)?[sessionId]
sessionContext = currentSessions.get(httpRequest.connection)?[sessionId]
if sessionData?
return callback(null, sessionData)
if sessionContext?
return callback(null, sessionContext)
setTimeout(() ->
db.redisClient.eval('local v=redis.call("get",KEYS[1]);redis.call("del",KEYS[1]);return v;', 1, "session_#{sessionId}", (err, sessionDataString) ->
return callback(err) if err or not sessionDataString
session.deserialize(sessionDataString, (err, sessionData) ->
db.redisClient.eval('local v=redis.call("get",KEYS[1]);redis.call("del",KEYS[1]);return v;', 1, "session_#{sessionId}", (err, sessionContextString) ->
return callback(err) if err or not sessionContextString
session.deserialize(sessionContextString, (err, sessionContext) ->
return callback(err) if err
currentSessions.get(httpRequest.connection)[sessionId] = sessionData
callback(null, sessionData)
currentSessions.get(httpRequest.connection)[sessionId] = sessionContext
callback(null, sessionContext)
)
)
, 100
@@ -569,11 +572,13 @@ onConnection = (socket) ->
currentSessions.set(socket, {})
socket.on('close', () ->
sessions = currentSessions.get(socket)
for sessionId, sessionData of sessions
session.serialize(sessionData, (err, sessionDataString) ->
for sessionId, sessionContext of sessions
delete sessionContext.httpRequest
delete sessionContext.httpResponse
session.serialize(sessionContext, (err, sessionContextString) ->
return throwError(err) if err
# TODO don't set if process is shutting down
db.redisClient.setex("session_#{sessionId}", sessionData.timeout, sessionDataString, (err) ->
db.redisClient.setex("session_#{sessionId}", sessionContext.timeout, sessionContextString, (err) ->
return throwError(err) if err
)
)
@@ -618,121 +623,114 @@ listener = (httpRequest, httpResponse) ->
return body
stream.on('end', () ->
cwmpRequest = null
getSession(httpRequest, f = (err, sessionData) ->
rpc = null
getSession(httpRequest, f = (err, sessionContext) ->
return throwError(err, httpResponse) if err
cwmpRequest ?= soap.request(httpRequest, sessionData?.cwmpVersion)
if not sessionData?
if cwmpRequest.methodRequest?.type isnt 'Inform'
rpc ?= soap.request(httpRequest, sessionContext?.cwmpVersion)
if not sessionContext?
if rpc.cpeRequest?.name isnt 'Inform'
httpResponse.writeHead(400)
httpResponse.end('Session is expired')
return
deviceId = common.generateDeviceId(cwmpRequest.methodRequest.deviceId)
return session.init(deviceId, cwmpRequest.cwmpVersion, cwmpRequest.sessionTimeout ? config.get('SESSION_TIMEOUT', deviceId), (err, sessionData) ->
deviceId = common.generateDeviceId(rpc.cpeRequest.deviceId)
return session.init(deviceId, rpc.cwmpVersion, rpc.sessionTimeout ? config.get('SESSION_TIMEOUT', deviceId), (err, sessionContext) ->
return throwError(err, httpResponse) if err
sessionData.sessionId = crypto.randomBytes(8).toString('hex')
sessionContext.sessionId = crypto.randomBytes(8).toString('hex')
currentSessions.get(httpRequest.connection)[sessionData.sessionId] = sessionData
currentSessions.get(httpRequest.connection)[sessionContext.sessionId] = sessionContext
httpRequest.connection.setTimeout(sessionData.timeout * 1000)
httpRequest.connection.setTimeout(sessionContext.timeout * 1000)
db.getDueTasksAndFaultsAndOperations(deviceId, sessionData.timestamp, (err, dueTasks, faults, operations) ->
db.getDueTasksAndFaultsAndOperations(deviceId, sessionContext.timestamp, (err, dueTasks, faults, operations) ->
return throwError(err, httpResponse) if err
sessionData.tasks = dueTasks
sessionData.faults = faults
sessionData.retries = {}
sessionContext.tasks = dueTasks
sessionContext.faults = faults
sessionContext.retries = {}
for k, v of faults
sessionData.retries[k] = v.retries
sessionData.operations = operations
sessionContext.retries[k] = v.retries
sessionContext.operations = operations
# Delete expired faults
for k, v of sessionData.faults
if v.expiry >= sessionData.timestamp
delete sessionData.faults[k]
sessionData.faultsTouched ?= {}
sessionData.faultsTouched[k] = true
for k, v of sessionContext.faults
if v.expiry >= sessionContext.timestamp
delete sessionContext.faults[k]
sessionContext.faultsTouched ?= {}
sessionContext.faultsTouched[k] = true
f(null, sessionData)
f(null, sessionContext)
)
)
currentRequest = {
httpRequest : httpRequest,
httpResponse : httpResponse,
sessionData : sessionData
}
sessionContext.httpRequest = httpRequest
sessionContext.httpResponse = httpResponse
if config.get('DEBUG', currentRequest.sessionData.deviceId)
if config.get('DEBUG', sessionContext.deviceId)
dump = "# REQUEST #{new Date(Date.now())}\n" + JSON.stringify(httpRequest.headers) + "\n#{httpRequest.getBody()}\n\n"
require('fs').appendFile("./debug/#{currentRequest.sessionData.deviceId}.dump", dump, (err) ->
require('fs').appendFile("./debug/#{sessionContext.deviceId}.dump", dump, (err) ->
return throwError(err) if err
)
if cwmpRequest.methodRequest?
if cwmpRequest.methodRequest.type is 'Inform'
if rpc.cpeRequest?
if rpc.cpeRequest.name is 'Inform'
logger.accessInfo({
currentRequest: currentRequest
sessionContext: sessionContext
message: 'Inform'
cpeRequest: cwmpRequest.methodRequest
cpeRequestId: cwmpRequest.id
});
inform(currentRequest, cwmpRequest)
else if cwmpRequest.methodRequest.type is 'TransferComplete'
logger.accessInfo({
currentRequest: currentRequest
message: 'CPE request'
cpeRequest: cwmpRequest.methodRequest
cpeRequestId: cwmpRequest.id
});
transferComplete(currentRequest, cwmpRequest)
else if cwmpRequest.methodRequest.type is 'GetRPCMethods'
logger.accessInfo({
currentRequest: currentRequest
message: 'CPE request'
cpeRequest: cwmpRequest.methodRequest
cpeRequestId: cwmpRequest.id
});
res = soap.response({
id : cwmpRequest.id,
methodResponse : {type : 'GetRPCMethodsResponse', methodList : ['Inform', 'GetRPCMethods', 'TransferComplete', 'RequestDownload']},
cwmpVersion : currentRequest.sessionData.cwmpVersion
rpc: rpc
})
writeResponse(currentRequest, res)
inform(sessionContext, rpc)
else if rpc.cpeRequest.name is 'TransferComplete'
logger.accessInfo({
sessionContext: sessionContext
message: 'CPE request'
rpc: rpc
})
transferComplete(sessionContext, rpc)
else if rpc.cpeRequest.name is 'GetRPCMethods'
logger.accessInfo({
sessionContext: sessionContext
message: 'CPE request'
rpc: rpc
})
res = soap.response({
id : rpc.id
acsResponse : {name: 'GetRPCMethodsResponse', methodList: ['Inform', 'GetRPCMethods', 'TransferComplete', 'RequestDownload']}
cwmpVersion : sessionContext.cwmpVersion
})
writeResponse(sessionContext, res)
else
return throwError(new Error('ACS method not supported'), currentRequest.httpResponse)
else if cwmpRequest.methodResponse?
session.rpcResponse(currentRequest.sessionData, cwmpRequest.id, cwmpRequest.methodResponse, (err) ->
return throwError(err, currentRequest.httpResponse) if err
nextRpc(currentRequest)
return throwError(new Error('ACS method not supported'), sessionContext.httpResponse)
else if rpc.cpeResponse
session.rpcResponse(sessionContext, rpc.id, rpc.cpeResponse, (err) ->
return throwError(err, sessionContext.httpResponse) if err
nextRpc(sessionContext)
)
else if cwmpRequest.fault?
else if rpc.cpeFault
logger.accessWarn({
currentRequest: currentRequest
sessionContext: sessionContext
message: 'CPE fault'
cpeFault: cwmpRequest.fault.detail
cpeRequestId: cwmpRequest.id
});
session.rpcFault(currentRequest.sessionData, cwmpRequest.id, cwmpRequest.fault, (err, fault) ->
return throwError(err, currentRequest.httpResponse) if err
rpc:rpc
})
session.rpcFault(sessionContext, rpc.id, rpc.cpeFault, (err, fault) ->
return throwError(err, sessionContext.httpResponse) if err
if fault
recordFault(currentRequest, fault, currentRequest.sessionData.provisions, currentRequest.sessionData.channels)
session.clearProvisions(currentRequest.sessionData)
nextRpc(currentRequest)
recordFault(sessionContext, fault)
session.clearProvisions(sessionContext)
nextRpc(sessionContext)
)
else # CPE sent empty response
session.timeoutOperations(currentRequest.sessionData, (err, faults, operations) ->
return throwError(err, currentRequest.httpResponse) if err
session.timeoutOperations(sessionContext, (err, faults, operations) ->
return throwError(err, sessionContext.httpResponse) if err
for fault, i in faults
for k, v of operations[i].retries
sessionData.retries[k] = v
sessionContext.retries[k] = v
recordFault(currentRequest, fault, operations[i].provisions, operations[i].channels)
recordFault(sessionContext, fault, operations[i].provisions, operations[i].channels)
nextRpc(currentRequest)
nextRpc(sessionContext)
)
)
)
+13 -13
View File
@@ -23,14 +23,14 @@ const device = require("./device");
const MAX_DEPTH = config.get("MAX_DEPTH");
function refresh(sessionData, provision, declarations, startRevision, endRevision) {
function refresh(sessionContext, provision, declarations, startRevision, endRevision) {
let path = common.parsePath(provision[1]).slice();
let l = path.length;
path.length = MAX_DEPTH;
path.fill("*", l);
let every = 1000 * (provision[2] || 1);
let offset = Math.abs(common.djb2(sessionData.deviceId)) % 86400000;
let t = Math.trunc((sessionData.timestamp + offset) / every) * every - offset;
let offset = Math.abs(common.djb2(sessionContext.deviceId)) % 86400000;
let t = Math.trunc((sessionContext.timestamp + offset) / every) * every - offset;
for (let i = l; i < path.length; ++ i)
declarations.push([path.slice(0, i), t, {object: 1, writable: 1, value: t}]);
@@ -39,7 +39,7 @@ function refresh(sessionData, provision, declarations, startRevision, endRevisio
}
function value(sessionData, provision, declarations, startRevision, endRevision) {
function value(sessionContext, provision, declarations, startRevision, endRevision) {
let path = common.parsePath(provision[1]);
declarations.push([path, 1, {value: 1}, null, {value: [provision[2]]}]);
@@ -47,7 +47,7 @@ function value(sessionData, provision, declarations, startRevision, endRevision)
}
function tag(sessionData, provision, declarations, startRevision, endRevision) {
function tag(sessionContext, provision, declarations, startRevision, endRevision) {
let path = ["Tags", provision[1]];
declarations.push([path, 1, {value: 1}, null, {value: [provision[2]]}]);
@@ -55,33 +55,33 @@ function tag(sessionData, provision, declarations, startRevision, endRevision) {
}
function reboot(sessionData, provision, declarations, startRevision, endRevision) {
declarations.push([["Reboot"], 1, {value: 1}, null, {value: [sessionData.timestamp]}]);
function reboot(sessionContext, provision, declarations, startRevision, endRevision) {
declarations.push([["Reboot"], 1, {value: 1}, null, {value: [sessionContext.timestamp]}]);
return true;
}
function reset(sessionData, provision, declarations, startRevision, endRevision) {
declarations.push([["FactoryReset"], 1, {value: 1}, null, {value: [sessionData.timestamp]}]);
function reset(sessionContext, provision, declarations, startRevision, endRevision) {
declarations.push([["FactoryReset"], 1, {value: 1}, null, {value: [sessionContext.timestamp]}]);
return true;
}
function download(sessionData, provision, declarations, startRevision, endRevision) {
function download(sessionContext, provision, declarations, startRevision, endRevision) {
let alias = ["FileType", provision[1] || "", "FileName", provision[2] || "",
"TargetFileName", provision[3] || ""];
declarations.push([["Downloads", alias], 1, {}, 1]);
declarations.push([["Downloads", alias, "Download"], 1, {value: 1}, null,
{value: [sessionData.timestamp]}]);
{value: [sessionContext.timestamp]}]);
return true;
}
function instances(sessionData, provision, declarations, startRevision, endRevision) {
function instances(sessionContext, provision, declarations, startRevision, endRevision) {
let count = Number(provision[2]);
if (Number.isNaN(count))
@@ -95,7 +95,7 @@ function instances(sessionData, provision, declarations, startRevision, endRevis
if (endRevision === startRevision)
return false;
let unpacked = device.unpack(sessionData.deviceData, path, startRevision + 1);
let unpacked = device.unpack(sessionContext.deviceData, path, startRevision + 1);
count = Math.max(0, unpacked.length + count);
}
+28 -26
View File
@@ -131,10 +131,10 @@ function close() {
function flatten(details) {
if (details.currentRequest) {
details.deviceId = details.currentRequest.sessionData.deviceId;
details.remoteAddress = details.currentRequest.httpRequest.connection.remoteAddress;
delete details.currentRequest;
if (details.sessionContext) {
details.deviceId = details.sessionContext.deviceId;
details.remoteAddress = details.sessionContext.httpRequest.connection.remoteAddress;
delete details.sessionContext;
}
if (details.exception) {
@@ -149,30 +149,31 @@ function flatten(details) {
delete details.task;
}
if (details.acsRequest) {
details.acsRequestName = details.acsRequest.type;
if (details.acsRequest.commandKey)
details.acsRequestCommandKey = details.acsRequest.commandKey;
delete details.acsRequest;
}
if (details.cpeRequest) {
if (details.cpeRequest.type === "Inform") {
details.informEvent = details.cpeRequest.event.join(",");
details.informRetryCount = details.cpeRequest.retryCount;
if (details.rpc) {
if (details.rpc.acsRequest) {
details.acsRequestId = details.rpc.id;
details.acsRequestName = details.rpc.acsRequest.name;
if (details.rpc.acsRequest.commandKey)
details.acsRequestCommandKey = details.rpc.acsRequest.commandKey;
}
else {
details.cpeRequestName = details.cpeRequest.type;
if (details.cpeRequest.commandKey)
details.cpeRequestCommandKey = details.cpeRequest.commandKey;
else if (details.rpc.cpeRequest) {
details.cpeRequestId = details.rpc.id;
if (details.rpc.cpeRequest.name === "Inform") {
details.informEvent = details.rpc.cpeRequest.event.join(",");
details.informRetryCount = details.rpc.cpeRequest.retryCount;
}
else {
details.cpeRequestName = details.rpc.cpeRequest.type;
if (details.rpc.cpeRequest.commandKey)
details.cpeRequestCommandKey = details.rpc.cpeRequest.commandKey;
}
}
delete details.cpeRequest;
}
if (details.cpeFault) {
details.cpeFaultCode = details.cpeFault.faultCode;
details.cpeFaultString = details.cpeFault.faultString;
delete details.cpeFault;
else if (details.rpc.cpeFault) {
details.acsRequestId = details.rpc.id;
details.cpeFaultCode = details.rpc.cpeFault.faultCode;
details.cpeFaultString = details.rpc.cpeFault.faultString;
}
delete details.rpc;
}
if (details.fault) {
@@ -180,6 +181,7 @@ function flatten(details) {
details.faultMessage = details.fault.message;
delete details.fault;
}
return details;
}
+426 -425
View File
File diff suppressed because it is too large. Load diff
-464
View File
@@ -1,464 +0,0 @@
###
# 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'
SERVER_NAME = "GenieACS/#{require('../package.json').version}"
NAMESPACES = {
'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'
}
}
LIBXMLJS_PARSE_OPTIONS = {
'nocdata' : true,
'noent' : true,
'recover' : config.get('XML_RECOVER'),
'ignore_enc' : config.get('XML_IGNORE_ENC')
}
LIBXMLJS_SAVE_OPTIONS = {
'format' : config.get('XML_FORMAT'),
'declaration' : not config.get('XML_NO_DECL'),
'selfCloseEmpty' : not config.get('XML_NO_EMPTY')
}
XML_IGNORE_NAMESPACE = config.get('XML_IGNORE_NAMESPACE')
# Workaround for devices that don't use correct namespace declarations or prefixes
if XML_IGNORE_NAMESPACE
libxmljs.Element.prototype.__find = libxmljs.Element.prototype.find
libxmljs.Element.prototype.find = (xpath, namespaces) ->
# Modify xpath queries to work regardless of element's namespace
p = xpath.replace(/([^\/:]+:)?([^\/]+)(\/|$)/g, '$2$3') \
.replace(/([a-zA-Z0-9_-]+)([^\/]*)(\/|$)/g, "*[local-name()='$1']$2$3")
libxmljs.Element.prototype.__find.call(this, p, namespaces)
event = (xml) ->
e.text().trim() for e in xml.find('EventStruct/EventCode')
parameterInfoList = (xml) ->
infoList = []
for e in xml.find('ParameterInfoStruct')
name = e.get('Name').text().trim()
writable = Boolean(JSON.parse(e.get('Writable').text()))
infoList.push([name, writable])
return infoList
parameterValueList = (xml) ->
valueList = []
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()
try
switch valueType
when 'xsd:boolean'
value = Boolean(JSON.parse(value))
when 'xsd:unsignedInt'
value = JSON.parse(value)
when 'xsd:int'
value = JSON.parse(value)
catch err
value = null
valueList.push([name, value, valueType])
return valueList
cpeGetParameterNames = (xml, methodRequest) ->
el = xml.node('cwmp:GetParameterNames')
el.node('ParameterPath').text(methodRequest.parameterPath)
el.node('NextLevel').text(+methodRequest.nextLevel)
cpeGetParameterNamesResponse = (xml) ->
{parameterList : parameterInfoList(xml.get('ParameterList'))}
cpeGetParameterValues = (xml, methodRequest) ->
el = xml.node('cwmp:GetParameterValues').node('ParameterNames')
el.attr({'soap-enc:arrayType' : "xsd:string[#{methodRequest.parameterNames.length}]"})
for p in methodRequest.parameterNames
el.node('string').text(p)
cpeGetParameterValuesResponse = (xml) ->
{parameterList : parameterValueList(xml.get('ParameterList'))}
cpeSetParameterValues = (xml, methodRequest) ->
el = xml.node('cwmp:SetParameterValues')
paramList = el.node('ParameterList')
paramList.attr({'soap-enc:arrayType' : "cwmp:ParameterValueStruct[#{methodRequest.parameterList.length}]"})
for i in methodRequest.parameterList
pvs = paramList.node('ParameterValueStruct')
pvs.node('Name').text(i[0])
v = pvs.node('Value')
v.text(i[1])
v.attr({'xsi:type' : i[2]}) if i[2]?
el.node('ParameterKey').text(methodRequest.parameterKey ? '')
cpeSetParameterValuesResponse = (xml) ->
{status : JSON.parse(xml.get('Status').text())}
cpeAddObject = (xml, methodRequest) ->
el = xml.node('cwmp:AddObject')
el.node('ObjectName').text(methodRequest.objectName)
el.node('ParameterKey').text(methodRequest.parameterKey ? '')
cpeAddObjectResponse = (xml) ->
{
instanceNumber : parseInt(xml.get('InstanceNumber').text()),
status : parseInt(xml.get('Status').text())
}
cpeDeleteObject = (xml, methodRequest) ->
el = xml.node('cwmp:DeleteObject')
el.node('ObjectName').text(methodRequest.objectName)
el.node('ParameterKey').text(methodRequest.parameterKey ? '')
cpeDeleteObjectResponse = (xml) ->
{status : parseInt(xml.get('Status').text())}
cpeReboot = (xml, methodRequest) ->
el = xml.node('cwmp:Reboot')
el.node('CommandKey').text(methodRequest.commandKey ? '')
cpeRebootResponse = (xml) ->
{}
cpeFactoryReset = (xml, methodRequest) ->
el = xml.node('cwmp:FactoryReset')
cpeFactoryResetResponse = (xml, methodRequest) ->
{}
cpeDownload = (xml, methodRequest) ->
el = xml.node('cwmp:Download')
el.node('CommandKey').text(methodRequest.commandKey ? '')
el.node('FileType').text(methodRequest.fileType)
el.node('URL').text(methodRequest.url)
el.node('Username').text(methodRequest.username ? '')
el.node('Password').text(methodRequest.password ? '')
el.node('FileSize').text(methodRequest.fileSize ? '0')
el.node('TargetFileName').text(methodRequest.targetFileName ? '')
el.node('DelaySeconds').text(methodRequest.delaySeconds ? '0')
el.node('SuccessURL').text(methodRequest.successUrl ? '')
el.node('FailureURL').text(methodRequest.failureUrl ? '')
cpeDownloadResponse = (xml) ->
res = {
status : parseInt(xml.get('Status').text()),
}
if res.status == 0
res.startTime = Date.parse(xml.get('StartTime').text())
res.completeTime = Date.parse(xml.get('CompleteTime').text())
return res
acsInform = (xml) ->
{
parameterList : parameterValueList(xml.get('ParameterList')),
deviceId : {
'Manufacturer' : xml.get('DeviceId/Manufacturer').text(),
'OUI' : xml.get('DeviceId/OUI').text(),
'ProductClass' : xml.get('DeviceId/ProductClass').text(),
'SerialNumber' : xml.get('DeviceId/SerialNumber').text()
}
event : event(xml.get('Event')),
retryCount : parseInt(xml.get('RetryCount').text())
}
acsInformResponse = (xml, methodResponse) ->
xml.node('cwmp:InformResponse').node('MaxEnvelopes').text(1)
acsGetRPCMethods = (xml) ->
{}
acsGetRPCMethodsResponse = (xml, methodResponse) ->
el = xml.node('cwmp:GetRPCMethodsResponse').node('MethodList')
el.attr({'soap-enc:arrayType' : "xsd:string[#{methodResponse.methodList.length}]"})
for m in methodResponse.methodList
el.node('string').text(m)
acsTransferComplete = (xml) ->
{
commandKey : xml.get('CommandKey').text(),
faultStruct : faultStruct(xml.get('FaultStruct')),
startTime : Date.parse(xml.get('StartTime').text()),
completeTime : Date.parse(xml.get('CompleteTime').text())
}
acsTransferCompleteResponse = (xml, methodResponse) ->
xml.node('cwmp:TransferCompleteResponse').text('')
acsRequestDownload = (xml) ->
# TODO FileTypeArg
{
fileType : xml.get('FileType').text()
}
acsRequestDownloadResponse = (xml, methodResponse) ->
xml.node('cwmp:RequestDownloadResponse').text('')
faultStruct = (xml) ->
f = {
faultCode: xml.get('FaultCode').text(),
faultString: xml.get('FaultString').text()
}
for e in xml.find('SetParameterValuesFault')
f.setParameterValuesFault ?= []
f.setParameterValuesFault.push({
parameterName: e.get('ParameterName').text(),
faultCode: e.get('FaultCode').text(),
faultString: e.get('FaultString').text()
})
return f
fault = (xml, cwmpVersion) ->
{
faultCode: xml.get('faultcode').text(),
faultString: xml.get('faultstring').text(),
detail: faultStruct(xml.get('detail/cwmp:Fault', NAMESPACES[cwmpVersion]))
}
exports.request = (httpRequest, cwmpVersion) ->
cwmpRequest = {cwmpVersion : cwmpVersion}
data = httpRequest.getBody()
if data.length > 0
xml = libxmljs.parseXml(data, LIBXMLJS_PARSE_OPTIONS)
if not cwmpRequest.cwmpVersion?
# 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'
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
if methodElement? and not (XML_IGNORE_NAMESPACE and methodElement.name() is 'Fault')
switch methodElement.name()
when 'Inform'
cwmpRequest.methodRequest = acsInform(methodElement)
cwmpRequest.methodRequest.type = 'Inform'
when 'GetRPCMethods'
cwmpRequest.methodRequest = acsGetRPCMethods(methodElement)
cwmpRequest.methodRequest.type = 'GetRPCMethods'
when 'TransferComplete'
cwmpRequest.methodRequest = acsTransferComplete(methodElement)
cwmpRequest.methodRequest.type = 'TransferComplete'
when 'RequestDownload'
cwmpRequest.methodRequest = acsRequestDownload(methodElement)
cwmpRequest.methodRequest.type = 'RequestDownload'
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 'AddObjectResponse'
cwmpRequest.methodResponse = cpeAddObjectResponse(methodElement)
cwmpRequest.methodResponse.type = 'AddObjectResponse'
when 'DeleteObjectResponse'
cwmpRequest.methodResponse = cpeDeleteObjectResponse(methodElement)
cwmpRequest.methodResponse.type = 'DeleteObjectResponse'
when 'RebootResponse'
cwmpRequest.methodResponse = cpeRebootResponse(methodElement)
cwmpRequest.methodResponse.type = 'RebootResponse'
when 'FactoryResetResponse'
cwmpRequest.methodResponse = cpeFactoryResetResponse(methodElement)
cwmpRequest.methodResponse.type = 'FactoryResetResponse'
when 'DownloadResponse'
cwmpRequest.methodResponse = cpeDownloadResponse(methodElement)
cwmpRequest.methodResponse.type = 'DownloadResponse'
else
throw Error('8000 Method not supported ' + methodElement.name())
else
faultElement = xml.get('/soap-env:Envelope/soap-env:Body/soap-env:Fault', NAMESPACES[cwmpRequest.cwmpVersion])
cwmpRequest.fault = fault(faultElement, cwmpRequest.cwmpVersion)
return cwmpRequest
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 = {
'Server' : SERVER_NAME,
'SOAPServer' : SERVER_NAME,
}
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'
acsInformResponse(body, cwmpResponse.methodResponse)
when 'GetRPCMethodsResponse'
acsGetRPCMethodsResponse(body, cwmpResponse.methodResponse)
when 'TransferCompleteResponse'
acsTransferCompleteResponse(body, cwmpResponse.methodResponse)
when 'RequestDownloadResponse'
acsRequestDownloadResponse(body, cwmpResponse.methodResponse)
else
throw Error("Unknown method response type #{cwmpResponse.methodResponse.type}")
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)
when 'GetParameterValues'
cpeGetParameterValues(body, cwmpResponse.methodRequest)
when 'SetParameterValues'
cpeSetParameterValues(body, cwmpResponse.methodRequest)
when 'AddObject'
cpeAddObject(body, cwmpResponse.methodRequest)
when 'DeleteObject'
cpeDeleteObject(body, cwmpResponse.methodRequest)
when 'Reboot'
cpeReboot(body, cwmpResponse.methodRequest)
when 'FactoryReset'
cpeFactoryReset(body, cwmpResponse.methodRequest)
when 'Download'
cpeDownload(body, cwmpResponse.methodRequest)
else
throw Error("Unknown method request #{cwmpResponse.methodRequest.type}")
if env?
headers['Content-Type'] = 'text/xml; charset="utf-8"'
return {code: 200, headers: headers, data: new Buffer(env.doc().toString(LIBXMLJS_SAVE_OPTIONS))}
else
return {code: 204, headers: headers, data: new Buffer(0)}
+559
View File
@@ -0,0 +1,559 @@
/**
* Copyright 2013-2017 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/>.
*/
"use strict";
const libxmljs = require("libxmljs");
const config = require("./config");
const SERVER_NAME = `GenieACS/${require("../package.json").version}`;
const NAMESPACES = {
"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"
}
};
const LIBXMLJS_PARSE_OPTIONS = {
"nocdata": true,
"noent": true,
"recover": config.get("XML_RECOVER"),
"ignore_enc": config.get("XML_IGNORE_ENC")
};
const LIBXMLJS_SAVE_OPTIONS = {
"format": config.get("XML_FORMAT"),
"declaration": !config.get("XML_NO_DECL"),
"selfCloseEmpty": !config.get("XML_NO_EMPTY")
};
const XML_IGNORE_NAMESPACE = config.get("XML_IGNORE_NAMESPACE");
if (XML_IGNORE_NAMESPACE) {
libxmljs.Element.prototype.__find = libxmljs.Element.prototype.find;
libxmljs.Element.prototype.find = function(xpath, namespaces) {
const p = xpath.replace(/([^\/:]+:)?([^\/]+)(\/|$)/g, "$2$3").replace(/([a-zA-Z0-9_-]+)([^\/]*)(\/|$)/g, "*[local-name()=\"$1\"]$2$3");
return libxmljs.Element.prototype.__find.call(this, p, namespaces);
};
}
function parseBool(v) {
v = "" + v;
if (v === "true" || v === "TRUE" || v === "True" || v === "1")
return true;
else if (v === "false" || v === "FALSE" || v === "False" || v === "0")
return false;
else
throw new Error("Invalid boolean value");
}
function event(xml) {
return xml.find("EventStruct/EventCode").map(e => e.text().trim());
}
function parameterInfoList(xml) {
return xml.find("ParameterInfoStruct").map(
e => [e.get("Name").text(), parseBool(e.get("Writable").text())]
);
}
function parameterValueList(xml) {
return xml.find("ParameterValueStruct").map(function(e) {
const valueElement = e.get("Value");
const valueType = valueElement.attr("type").value();
let value = valueElement.text();
if (valueType === "xsd:boolean")
value = parseBool(value);
else if (valueType === "xsd:int" || valueType === "xsd:unsignedInt")
value = parseInt(value);
else if (valueType === "xsd:dateTime")
value = Date.parse(value);
return [e.get("Name").text(), value, valueType];
});
}
function GetParameterNames(xml, methodRequest) {
const el = xml.node("cwmp:GetParameterNames");
el.node("ParameterPath").text(methodRequest.parameterPath);
el.node("NextLevel").text(+methodRequest.nextLevel);
}
function GetParameterNamesResponse(xml) {
return {
name: "GetParameterNamesResponse",
parameterList: parameterInfoList(xml.get("ParameterList"))
};
}
function GetParameterValues(xml, methodRequest) {
const el = xml.node("cwmp:GetParameterValues").node("ParameterNames");
el.attr({
"soap-enc:arrayType": `xsd:string[${methodRequest.parameterNames.length}]`
});
for (const p of methodRequest.parameterNames)
el.node("string").text(p);
}
function GetParameterValuesResponse(xml) {
return {
name: "GetParameterValuesResponse",
parameterList: parameterValueList(xml.get("ParameterList"))
};
}
function SetParameterValues(xml, methodRequest) {
const el = xml.node("cwmp:SetParameterValues");
const paramList = el.node("ParameterList");
paramList.attr({
"soap-enc:arrayType": `cwmp:ParameterValueStruct[${methodRequest.parameterList.length}]`
});
for (const p of methodRequest.parameterList) {
const pvs = paramList.node("ParameterValueStruct");
pvs.node("Name").text(p[0]);
pvs.node("Value").attr({"xsi:type": p[2]}).text(p[1]);
}
el.node("ParameterKey").text(methodRequest.parameterKey || "");
}
function SetParameterValuesResponse(xml) {
return {
name: "SetParameterValuesResponse",
status: parseInt(xml.get("Status").text())
};
}
function AddObject(xml, methodRequest) {
const el = xml.node("cwmp:AddObject");
el.node("ObjectName").text(methodRequest.objectName);
el.node("ParameterKey").text(methodRequest.parameterKey || "");
}
function AddObjectResponse(xml) {
return {
name: "AddObjectResponse",
instanceNumber: parseInt(xml.get("InstanceNumber").text()),
status: parseInt(xml.get("Status").text())
};
}
function DeleteObject(xml, methodRequest) {
const el = xml.node("cwmp:DeleteObject");
el.node("ObjectName").text(methodRequest.objectName);
el.node("ParameterKey").text(methodRequest.parameterKey || "");
}
function DeleteObjectResponse(xml) {
return {
name: "DeleteObjectResponse",
status: parseInt(xml.get("Status").text())
};
}
function Reboot(xml, methodRequest) {
const el = xml.node("cwmp:Reboot");
el.node("CommandKey").text(methodRequest.commandKey || "");
}
function RebootResponse(xml) {
return {
name: "RebootResponse"
};
}
function FactoryReset(xml, methodRequest) {
xml.node("cwmp:FactoryReset");
}
function FactoryResetResponse(xml, methodRequest) {
return {
name: "FactoryResetResponse"
};
}
function Download(xml, methodRequest) {
const el = xml.node("cwmp:Download");
el.node("CommandKey").text(methodRequest.commandKey || "");
el.node("FileType").text(methodRequest.fileType);
el.node("URL").text(methodRequest.url);
el.node("Username").text(methodRequest.username || "");
el.node("Password").text(methodRequest.password || "");
el.node("FileSize").text(methodRequest.fileSize || "0");
el.node("TargetFileName").text(methodRequest.targetFileName || "");
el.node("DelaySeconds").text(methodRequest.delaySeconds || "0");
el.node("SuccessURL").text(methodRequest.successUrl || "");
el.node("FailureURL").text(methodRequest.failureUrl || "");
}
function DownloadResponse(xml) {
const res = {
name: "DownloadResponse",
status: parseInt(xml.get("Status").text())
};
if (res.status === 0) {
res.startTime = Date.parse(xml.get("StartTime").text());
res.completeTime = Date.parse(xml.get("CompleteTime").text());
}
return res;
}
function Inform(xml) {
return {
name: "Inform",
parameterList: parameterValueList(xml.get("ParameterList")),
deviceId: {
"Manufacturer": xml.get("DeviceId/Manufacturer").text(),
"OUI": xml.get("DeviceId/OUI").text(),
"ProductClass": xml.get("DeviceId/ProductClass").text(),
"SerialNumber": xml.get("DeviceId/SerialNumber").text()
},
event: event(xml.get("Event")),
retryCount: parseInt(xml.get("RetryCount").text())
};
}
function InformResponse(xml, methodResponse) {
xml.node("cwmp:InformResponse").node("MaxEnvelopes").text(1);
}
function GetRPCMethods(xml) {
return {name: "GetRPCMethods"};
}
function GetRPCMethodsResponse(xml, methodResponse) {
const el = xml.node("cwmp:GetRPCMethodsResponse").node("MethodList");
el.attr({
"soap-enc:arrayType": `xsd:string[${methodResponse.methodList.length}]`
});
for (const m of methodResponse.methodList)
el.node("string").text(m);
}
function TransferComplete(xml) {
return {
name: "TransferComplete",
commandKey: xml.get("CommandKey").text(),
faultStruct: faultStruct(xml.get("FaultStruct")),
startTime: Date.parse(xml.get("StartTime").text()),
completeTime: Date.parse(xml.get("CompleteTime").text())
};
}
function TransferCompleteResponse(xml, methodResponse) {
xml.node("cwmp:TransferCompleteResponse").text("");
}
function RequestDownload(xml) {
return {
name: "RequestDownload",
fileType: xml.get("FileType").text()
};
}
function RequestDownloadResponse(xml, methodResponse) {
xml.node("cwmp:RequestDownloadResponse").text("");
}
function faultStruct(xml) {
const f = {
faultCode: xml.get("FaultCode").text(),
faultString: xml.get("FaultString").text()
};
for (const e of xml.find("SetParameterValuesFault")) {
if (!f.setParameterValuesFault)
f.setParameterValuesFault = [];
f.setParameterValuesFault.push({
parameterName: e.get("ParameterName").text(),
faultCode: e.get("FaultCode").text(),
faultString: e.get("FaultString").text()
});
}
return f;
}
function fault(xml, cwmpVersion) {
return {
faultCode: xml.get("faultcode").text(),
faultString: xml.get("faultstring").text(),
detail: faultStruct(xml.get("detail/cwmp:Fault", NAMESPACES[cwmpVersion]))
};
}
function request(httpRequest, cwmpVersion) {
const rpc = {
cwmpVersion: cwmpVersion
};
const data = httpRequest.getBody();
if (!data.length)
return rpc;
const xml = libxmljs.parseXml(data, LIBXMLJS_PARSE_OPTIONS);
let methodElement;
if (rpc.cwmpVersion) {
methodElement = xml.get("/soap-env:Envelope/soap-env:Body/cwmp:*", NAMESPACES[rpc.cwmpVersion]);
}
else {
// 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()) {
case "urn:dslforum-org:cwmp-1-0":
rpc.cwmpVersion = "1.0";
break;
case "urn:dslforum-org:cwmp-1-1":
rpc.cwmpVersion = "1.1";
break;
case "urn:dslforum-org:cwmp-1-2":
const timeoutElement = xml.get("/soap-env:Envelope/soap-env:Header/cwmp:sessionTimeout", NAMESPACES["1.2"]);
if (timeoutElement) {
rpc.sessionTimeout = parseInt(timeoutElement.text());
rpc.cwmpVersion = "1.3";
}
else {
rpc.cwmpVersion = "1.2";
}
break;
case "urn:dslforum-org:cwmp-1-3":
rpc.cwmpVersion = "1.4";
break;
default:
throw new Error("Unrecognized CWMP version");
}
if (!rpc.sessionTimeout) {
const timeoutElement = xml.get("/soap-env:Envelope/soap-env:Header/cwmp:sessionTimeout", NAMESPACES[rpc.cwmpVersion]);
if (timeoutElement)
rpc.sessionTimeout = parseInt(timeoutElement.text());
}
}
rpc.id = xml.get("/soap-env:Envelope/soap-env:Header/cwmp:ID", NAMESPACES[rpc.cwmpVersion]).text();
if (methodElement && !(XML_IGNORE_NAMESPACE && methodElement.name() === "Fault")) {
switch (methodElement.name()) {
case "Inform":
rpc.cpeRequest = Inform(methodElement);
break;
case "GetRPCMethods":
rpc.cpeRequest = GetRPCMethods(methodElement);
break;
case "TransferComplete":
rpc.cpeRequest = TransferComplete(methodElement);
break;
case "RequestDownload":
rpc.cpeRequest = RequestDownload(methodElement);
break;
case "GetParameterNamesResponse":
rpc.cpeResponse = GetParameterNamesResponse(methodElement);
break;
case "GetParameterValuesResponse":
rpc.cpeResponse = GetParameterValuesResponse(methodElement);
break;
case "SetParameterValuesResponse":
rpc.cpeResponse = SetParameterValuesResponse(methodElement);
break;
case "AddObjectResponse":
rpc.cpeResponse = AddObjectResponse(methodElement);
break;
case "DeleteObjectResponse":
rpc.cpeResponse = DeleteObjectResponse(methodElement);
break;
case "RebootResponse":
rpc.cpeResponse = RebootResponse(methodElement);
break;
case "FactoryResetResponse":
rpc.cpeResponse = FactoryResetResponse(methodElement);
break;
case "DownloadResponse":
rpc.cpeResponse = DownloadResponse(methodElement);
break;
default:
throw new Error(`8000 Method not supported ${methodElement.name()}`);
}
}
else {
const faultElement = xml.get("/soap-env:Envelope/soap-env:Body/soap-env:Fault", NAMESPACES[rpc.cwmpVersion]);
rpc.cpeFault = fault(faultElement, rpc.cwmpVersion);
}
return rpc;
}
function createSoapEnv(cwmpVersion) {
const xml = libxmljs.Document();
const env = xml.node("soap-env:Envelope");
for (const prefix of Object.keys(NAMESPACES[cwmpVersion]))
env.defineNamespace(prefix, NAMESPACES[cwmpVersion][prefix]);
return env;
}
function response(rpc) {
const headers = {
"Server": SERVER_NAME,
"SOAPServer": SERVER_NAME
};
if (!rpc)
return {code: 204, headers: headers, data: new Buffer(0)};
const env = createSoapEnv(rpc.cwmpVersion);
const header = env.node("soap-env:Header");
header.node("cwmp:ID").attr({
"soap-env:mustUnderstand": 1
}).text(rpc.id);
const body = env.node("soap-env:Body");
if (rpc.acsResponse) {
switch (rpc.acsResponse.name) {
case "InformResponse":
InformResponse(body, rpc.acsResponse);
break;
case "GetRPCMethodsResponse":
GetRPCMethodsResponse(body, rpc.acsResponse);
break;
case "TransferCompleteResponse":
TransferCompleteResponse(body, rpc.acsResponse);
break;
case "RequestDownloadResponse":
RequestDownloadResponse(body, rpc.acsResponse);
break;
default:
throw new Error(`Unknown method response type ${rpc.acsResponse.name}`);
}
}
else if (rpc.acsRequest) {
switch (rpc.acsRequest.name) {
case "GetParameterNames":
GetParameterNames(body, rpc.acsRequest);
break;
case "GetParameterValues":
GetParameterValues(body, rpc.acsRequest);
break;
case "SetParameterValues":
SetParameterValues(body, rpc.acsRequest);
break;
case "AddObject":
AddObject(body, rpc.acsRequest);
break;
case "DeleteObject":
DeleteObject(body, rpc.acsRequest);
break;
case "Reboot":
Reboot(body, rpc.acsRequest);
break;
case "FactoryReset":
FactoryReset(body, rpc.acsRequest);
break;
case "Download":
Download(body, rpc.acsRequest);
break;
default:
throw new Error(`Unknown method request ${rpc.acsRequest.name}`);
}
}
headers["Content-Type"] = "text/xml; charset=\"utf-8\"";
return {
code: 200,
headers: headers,
data: new Buffer(env.doc().toString(LIBXMLJS_SAVE_OPTIONS))
};
}
exports.request = request;
exports.response = response;