Log session timeout and connection drop

This commit is contained in:
Zaid Abdulla committed 2017-10-09 00:25:32 -07:00
1 parent ef712de9e3
commit 342138d92e
3 files changed
+216 -155

No files matched your search

+214 -152
View File
@@ -52,13 +52,37 @@ throwError = (err, httpResponse) ->
throw err throw err
writeResponse = (sessionContext, res) -> writeResponse = (sessionContext, res, close) ->
# Close connection after last request in session
res.headers['Connection'] = 'close' if close
if config.get('DEBUG', sessionContext.deviceId) if config.get('DEBUG', sessionContext.deviceId)
dump = "# RESPONSE #{new Date(Date.now())}\n" + JSON.stringify(res.headers) + "\n#{res.data}\n\n" dump = "# RESPONSE #{new Date(Date.now())}\n" + JSON.stringify(res.headers) + "\n#{res.data}\n\n"
require('fs').appendFile("./debug/#{sessionContext.deviceId}.dump", dump, (err) -> require('fs').appendFile("./debug/#{sessionContext.deviceId}.dump", dump, (err) ->
throwError(err) if err throwError(err) if err
) )
finish = () ->
stats.concurrentRequests -= 1
if sessionContext.httpRequest.connection.destroyed
logger.accessError({
sessionContext: sessionContext
message: 'Connection dropped'
})
else if close
endSession(sessionContext, (err, isNew) ->
return throwError(err) if err
if isNew
logger.accessInfo({
sessionContext: sessionContext
message: 'New device registered'
})
)
else
sessionContext.lastActivity = Date.now()
currentSessions.set(sessionContext.httpRequest.connection, sessionContext)
# respond using the same content-encoding as the request # respond using the same content-encoding as the request
if sessionContext.httpRequest.headers['content-encoding']? and res.data.length > 0 if sessionContext.httpRequest.headers['content-encoding']? and res.data.length > 0
switch sessionContext.httpRequest.headers['content-encoding'] switch sessionContext.httpRequest.headers['content-encoding']
@@ -75,17 +99,13 @@ writeResponse = (sessionContext, res) ->
res.headers['Content-Length'] = data.length res.headers['Content-Length'] = data.length
sessionContext.httpResponse.writeHead(res.code, res.headers) sessionContext.httpResponse.writeHead(res.code, res.headers)
sessionContext.httpResponse.end(data) sessionContext.httpResponse.end(data)
delete sessionContext.httpRequest finish()
delete sessionContext.httpResponse
stats.concurrentRequests -= 1
) )
else else
res.headers['Content-Length'] = res.data.length res.headers['Content-Length'] = res.data.length
sessionContext.httpResponse.writeHead(res.code, res.headers) sessionContext.httpResponse.writeHead(res.code, res.headers)
sessionContext.httpResponse.end(res.data) sessionContext.httpResponse.end(res.data)
delete sessionContext.httpRequest finish()
delete sessionContext.httpResponse
stats.concurrentRequests -= 1
recordFault = (sessionContext, fault, provisions, channels) -> recordFault = (sessionContext, fault, provisions, channels) ->
@@ -112,7 +132,7 @@ recordFault = (sessionContext, fault, provisions, channels) ->
sessionContext.faultsTouched ?= {} sessionContext.faultsTouched ?= {}
sessionContext.faultsTouched[channel] = true sessionContext.faultsTouched[channel] = true
logger.accessError({ logger.accessWarn({
sessionContext: sessionContext sessionContext: sessionContext
message: 'Channel has faulted' message: 'Channel has faulted'
fault: fault fault: fault
@@ -452,8 +472,6 @@ nextRpc = (sessionContext) ->
endSession = (sessionContext, callback) -> endSession = (sessionContext, callback) ->
delete currentSessions.get(sessionContext.httpRequest.connection)[sessionContext.sessionId]
saveCache = sessionContext.cacheUntil? saveCache = sessionContext.cacheUntil?
counter = 3 counter = 3
@@ -534,17 +552,7 @@ endSession = (sessionContext, callback) ->
sendAcsRequest = (sessionContext, id, acsRequest) -> sendAcsRequest = (sessionContext, id, acsRequest) ->
if not acsRequest? if not acsRequest?
return endSession(sessionContext, (err, isNew) -> return writeResponse(sessionContext, soap.response(null), true)
return throwError(err, sessionContext.httpResponse) if err
if isNew
logger.accessInfo({
sessionContext: sessionContext
message: 'New device registered'
})
writeResponse(sessionContext, soap.response(null))
)
if acsRequest.name is 'Download' if acsRequest.name is 'Download'
if not acsRequest.url? if not acsRequest.url?
@@ -585,42 +593,66 @@ sendAcsRequest = (sessionContext, id, acsRequest) ->
writeResponse(sessionContext, res) writeResponse(sessionContext, res)
getSession = (httpRequest, sessionId, callback) -> getSession = (connection, sessionId, callback) ->
return callback() if not sessionId? sessionContext = currentSessions.get(connection)
sessionContext = currentSessions.get(httpRequest.connection)?[sessionId] if sessionContext
currentSessions.delete(connection)
if sessionContext?
return callback(null, sessionContext) return callback(null, sessionContext)
return callback() if not sessionId
setTimeout(() -> setTimeout(() ->
cache.pop("session_#{sessionId}", (err, sessionContextString) -> cache.pop("session_#{sessionId}", (err, sessionContextString) ->
return callback(err) if err or not sessionContextString return callback(err) if err or not sessionContextString
session.deserialize(sessionContextString, (err, sessionContext) -> session.deserialize(sessionContextString, callback)
return callback(err) if err
currentSessions.get(httpRequest.connection)[sessionId] = sessionContext
callback(null, sessionContext)
)
) )
, 100 , 100)
)
currentSessions = new WeakMap() currentSessions = new WeakMap()
# When socket closes, store active sessions in cache # When socket closes, store active sessions in cache
onConnection = (socket) -> onConnection = (socket) ->
currentSessions.set(socket, {}) # The property remoteAddress may be undefined after the connection is
# closed, unless we read it at least once (caching?)
socket.remoteAddress
socket.on('close', () -> socket.on('close', () ->
sessions = currentSessions.get(socket) sessionContext = currentSessions.get(socket)
for sessionId, sessionContext of sessions return if not sessionContext
session.serialize(sessionContext, (err, sessionContextString) -> currentSessions.delete(socket)
now = Date.now()
lastActivity = sessionContext.lastActivity
timeoutMsg = logger.flatten({
sessionContext: sessionContext
message: 'Session timeout'
sessionTimestamp: sessionContext.timestamp
})
timeout = (sessionContext.lastActivity + sessionContext.timeout * 1000) - now
if timeout <= 0
return logger.accessError(timeoutMsg)
setTimeout(() ->
cache.get("session_#{sessionContext.sessionId}", (err, sessionContextString) ->
return throwError(err) if err return throwError(err) if err
# TODO don't set if process is shutting down return if not sessionContextString
cache.set("session_#{sessionId}", sessionContextString, sessionContext.timeout, (err) -> session.deserialize(sessionContextString, (err, sessionContext) ->
return throwError(err) if err return throwError(err) if err
if sessionContext.lastActivity == lastActivity
logger.accessError(timeoutMsg)
) )
) )
, timeout + 1000).unref()
session.serialize(sessionContext, (err, sessionContextString) ->
return throwError(err) if err
cache.set("session_#{sessionContext.sessionId}", sessionContextString, Math.ceil(timeout / 1000) + 3, (err) ->
return throwError(err) if err
)
)
) )
setInterval(() -> setInterval(() ->
@@ -701,10 +733,73 @@ cacheDueTasksAndFaultsAndOperations = (deviceId, tasks, faults, operations, cach
cache.set("#{deviceId}_tasks_faults_operations", JSON.stringify(v), ttl, callback) cache.set("#{deviceId}_tasks_faults_operations", JSON.stringify(v), ttl, callback)
processRequest = (sessionContext, rpc) ->
if rpc.cpeRequest?
if rpc.cpeRequest.name is 'Inform'
logger.accessInfo({
sessionContext: sessionContext
message: 'Inform'
rpc: rpc
})
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']}
cwmpVersion : sessionContext.cwmpVersion
})
writeResponse(sessionContext, res)
else
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 rpc.cpeFault
logger.accessWarn({
sessionContext: sessionContext
message: 'CPE fault'
rpc:rpc
})
session.rpcFault(sessionContext, rpc.id, rpc.cpeFault, (err, fault) ->
return throwError(err, sessionContext.httpResponse) if err
if fault
recordFault(sessionContext, fault)
session.clearProvisions(sessionContext)
nextRpc(sessionContext)
)
else # CPE sent empty response
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
sessionContext.retries[k] = v
recordFault(sessionContext, fault, operations[i].provisions, operations[i].channels)
nextRpc(sessionContext)
)
listener = (httpRequest, httpResponse) -> listener = (httpRequest, httpResponse) ->
stats.totalRequests += 1 stats.totalRequests += 1
if httpRequest.method != 'POST' if httpRequest.method != 'POST'
httpResponse.writeHead 405, {'Allow': 'POST'} httpResponse.writeHead 405, {'Allow': 'POST', 'Connection': 'close'}
httpResponse.end('405 Method Not Allowed') httpResponse.end('405 Method Not Allowed')
return return
@@ -716,7 +811,7 @@ listener = (httpRequest, httpResponse) ->
# If overloaded, ask CPE to retry in 60 seconds # If overloaded, ask CPE to retry in 60 seconds
if not sessionId and stats.concurrentRequests > MAX_CONCURRENT_REQUESTS if not sessionId and stats.concurrentRequests > MAX_CONCURRENT_REQUESTS
httpResponse.writeHead(503, {'Retry-after': 60}) httpResponse.writeHead(503, {'Retry-after': 60, 'Connection': 'close'})
httpResponse.end('503 Service Unavailable') httpResponse.end('503 Service Unavailable')
stats.droppedRequests += 1 stats.droppedRequests += 1
return return
@@ -728,12 +823,19 @@ listener = (httpRequest, httpResponse) ->
when 'deflate' when 'deflate'
stream = httpRequest.pipe(zlib.createInflate()) stream = httpRequest.pipe(zlib.createInflate())
else else
httpResponse.writeHead(415) httpResponse.writeHead(415, {'Connection': 'close'})
httpResponse.end('415 Unsupported Media Type') httpResponse.end('415 Unsupported Media Type')
return return
else else
stream = httpRequest stream = httpRequest
stats.concurrentRequests += 1
httpRequest.on('aborted', () ->
stats.concurrentRequests -= 1
# In some cases event end can be emitted after aborted event
httpRequest.removeAllListeners('end')
)
chunks = [] chunks = []
bytes = 0 bytes = 0
@@ -742,63 +844,15 @@ listener = (httpRequest, httpResponse) ->
bytes += chunk.length bytes += chunk.length
) )
httpRequest.getBody = () -> stream.on('end', () ->
# Write all chunks into a Buffer
body = new Buffer(bytes) body = new Buffer(bytes)
offset = 0 offset = 0
chunks.forEach((chunk) -> chunks.forEach((chunk) ->
chunk.copy(body, offset, 0, chunk.length) chunk.copy(body, offset, 0, chunk.length)
offset += chunk.length offset += chunk.length
) )
return body
stream.on('end', () ->
stats.concurrentRequests += 1
rpc = null
parseWarnings = []
getSession(httpRequest, sessionId, f = (err, sessionContext) ->
return throwError(err, httpResponse) if err
rpc ?= soap.request(httpRequest, sessionContext?.cwmpVersion, parseWarnings)
if not sessionContext?
if rpc.cpeRequest?.name isnt 'Inform'
httpResponse.writeHead(400)
httpResponse.end('Session is expired')
stats.concurrentRequests -= 1
return
stats.initiatedSessions += 1
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
sessionContext.sessionId = crypto.randomBytes(8).toString('hex')
currentSessions.get(httpRequest.connection)[sessionContext.sessionId] = sessionContext
httpRequest.connection.setTimeout(sessionContext.timeout * 1000)
getDueTasksAndFaultsAndOperations(deviceId, sessionContext.timestamp, (err, dueTasks, faults, operations, cacheUntil) ->
return throwError(err, httpResponse) if err
sessionContext.tasks = dueTasks
sessionContext.faults = faults
sessionContext.retries = {}
for k, v of faults
sessionContext.retries[k] = v.retries
sessionContext.operations = operations
sessionContext.cacheUntil = cacheUntil
# Delete expired faults
for k, v of sessionContext.faults
if v.expiry >= sessionContext.timestamp
delete sessionContext.faults[k]
sessionContext.faultsTouched ?= {}
sessionContext.faultsTouched[k] = true
f(null, sessionContext)
)
)
parsedRpc = (sessionContext, rpc, parseWarnings) ->
sessionContext.httpRequest = httpRequest sessionContext.httpRequest = httpRequest
sessionContext.httpResponse = httpResponse sessionContext.httpResponse = httpResponse
@@ -808,71 +862,79 @@ listener = (httpRequest, httpResponse) ->
logger.accessWarn(w) logger.accessWarn(w)
if config.get('DEBUG', sessionContext.deviceId) if config.get('DEBUG', sessionContext.deviceId)
dump = "# REQUEST #{new Date(Date.now())}\n" + JSON.stringify(httpRequest.headers) + "\n#{httpRequest.getBody()}\n\n" dump = "# REQUEST #{new Date(Date.now())}\n" + JSON.stringify(httpRequest.headers) + "\n#{body}\n\n"
require('fs').appendFile("./debug/#{sessionContext.deviceId}.dump", dump, (err) -> require('fs').appendFile("./debug/#{sessionContext.deviceId}.dump", dump, (err) ->
return throwError(err) if err return throwError(err) if err
) )
if rpc.cpeRequest? processRequest(sessionContext, rpc)
if rpc.cpeRequest.name is 'Inform'
logger.accessInfo({ getSession(httpRequest.connection, sessionId, (err, sessionContext) ->
sessionContext: sessionContext return throwError(err, httpResponse) if err
message: 'Inform'
rpc: rpc if sessionContext and (sessionContext.sessionId != sessionId or
}) sessionContext.lastActivity + sessionContext.timeout * 1000 < Date.now())
inform(sessionContext, rpc) logger.accessError({message: 'Invalid session', sessionContext: sessionContext})
else if rpc.cpeRequest.name is 'TransferComplete' httpResponse.writeHead(400, {'Connection': 'close'})
logger.accessInfo({ httpResponse.end('Invalid session')
sessionContext: sessionContext stats.concurrentRequests -= 1
message: 'CPE request' return
rpc: rpc
}) # Check again just in case device included old session ID from the previous session
transferComplete(sessionContext, rpc) if not sessionContext and stats.concurrentRequests > MAX_CONCURRENT_REQUESTS
else if rpc.cpeRequest.name is 'GetRPCMethods' httpResponse.writeHead(503, {'Retry-after': 60, 'Connection': 'close'})
logger.accessInfo({ httpResponse.end('503 Service Unavailable')
sessionContext: sessionContext stats.droppedRequests += 1
message: 'CPE request' stats.concurrentRequests -= 1
rpc: rpc return
})
res = soap.response({ parseWarnings = []
id : rpc.id rpc = soap.request(body, sessionContext?.cwmpVersion, parseWarnings)
acsResponse : {name: 'GetRPCMethodsResponse', methodList: ['Inform', 'GetRPCMethods', 'TransferComplete']}
cwmpVersion : sessionContext.cwmpVersion return parsedRpc(sessionContext, rpc, parseWarnings) if sessionContext
})
writeResponse(sessionContext, res) if rpc.cpeRequest?.name isnt 'Inform'
else msg = {
return throwError(new Error('ACS method not supported'), sessionContext.httpResponse) message: 'Invalid session'
else if rpc.cpeResponse sessionContext: sessionContext or {}
session.rpcResponse(sessionContext, rpc.id, rpc.cpeResponse, (err) -> }
return throwError(err, sessionContext.httpResponse) if err msg.sessionContext.httpRequest = httpRequest
nextRpc(sessionContext) msg.sessionContext.httpResponse = httpResponse
) logger.accessError(msg)
else if rpc.cpeFault httpResponse.writeHead(400, {'Connection': 'close'})
logger.accessWarn({ httpResponse.end('Invalid session')
sessionContext: sessionContext stats.concurrentRequests -= 1
message: 'CPE fault' return
rpc:rpc
}) stats.initiatedSessions += 1
session.rpcFault(sessionContext, rpc.id, rpc.cpeFault, (err, fault) -> deviceId = common.generateDeviceId(rpc.cpeRequest.deviceId)
return throwError(err, sessionContext.httpResponse) if err return session.init(deviceId, rpc.cwmpVersion, rpc.sessionTimeout ? config.get('SESSION_TIMEOUT', deviceId), (err, sessionContext) ->
return throwError(err, httpResponse) if err
if fault
recordFault(sessionContext, fault) sessionContext.sessionId = crypto.randomBytes(8).toString('hex')
session.clearProvisions(sessionContext) httpRequest.connection.setTimeout(sessionContext.timeout * 1000)
nextRpc(sessionContext)
) getDueTasksAndFaultsAndOperations(deviceId, sessionContext.timestamp, (err, dueTasks, faults, operations, cacheUntil) ->
else # CPE sent empty response return throwError(err, httpResponse) if err
session.timeoutOperations(sessionContext, (err, faults, operations) ->
return throwError(err, sessionContext.httpResponse) if err sessionContext.tasks = dueTasks
sessionContext.faults = faults
for fault, i in faults sessionContext.retries = {}
for k, v of operations[i].retries for k, v of faults
sessionContext.retries[k] = v sessionContext.retries[k] = v.retries
sessionContext.operations = operations
recordFault(sessionContext, fault, operations[i].provisions, operations[i].channels) sessionContext.cacheUntil = cacheUntil
nextRpc(sessionContext) # Delete expired faults
for k, v of sessionContext.faults
if v.expiry >= sessionContext.timestamp
delete sessionContext.faults[k]
sessionContext.faultsTouched ?= {}
sessionContext.faultsTouched[k] = true
parsedRpc(sessionContext, rpc, parseWarnings)
) )
)
) )
) )
+1
View File
@@ -311,6 +311,7 @@ function accessError(details) {
exports.init = init; exports.init = init;
exports.close = close; exports.close = close;
exports.flatten = flatten;
exports.info = info; exports.info = info;
exports.warn = warn; exports.warn = warn;
exports.error = error; exports.error = error;
+1 -3
View File
@@ -410,15 +410,13 @@ function fault(xml, cwmpVersion) {
} }
function request(httpRequest, cwmpVersion, warn) { function request(data, cwmpVersion, warn) {
warnings = warn; warnings = warn;
const rpc = { const rpc = {
cwmpVersion: cwmpVersion cwmpVersion: cwmpVersion
}; };
const data = httpRequest.getBody();
if (!data.length) if (!data.length)
return rpc; return rpc;